The Blockmap (#852)

* separating geometry classes

* 2d geometry; retirement of the *3D suffix

* makings of an early block map datastructure

* entities in a zone - players, corpses, vehicles, deployables, ground clutter, and buildings - divided between sectors of the zone upon creation, management, or mounting; superfluous messages to keep track of blockmap state, for now

* trait for entities to be added to the blockmap; internal entity data keeps track of current blockmap sector information; calls to add/remove/update functions changed

* modified pieces of environment into an entities that can be added to a block map and have a countable bounding region; fixes for vehicle control seat occcupant collection; fix for squad individual callback references (original issue still remains?)

* introduced the block map into various existijng game calculationa where target selection can be reduced by its probing

* he_mines and jammer_mines now trigger if a valid target is detected at the initial point of deploy; they also trigger later, after a valid target has moved into the arming range of the mine

* conversion of interactions with zone into a queued, periodic set of tasks

* explosive deployable control -> mine deployable control

* tests repaired and all tests working

* mostly comments and documentation

* amenities are now represented on the blockmap
This commit is contained in:
Fate-JH 2021-06-11 23:02:48 -04:00 committed by GitHub
parent 8bf0c4cbff
commit 3966b0264d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 2701 additions and 1011 deletions

View file

@ -6,6 +6,7 @@ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed}
import akka.pattern.ask
import akka.util.Timeout
import net.psforever.actors.net.MiddlewareActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.login.WorldSession._
import net.psforever.objects._
import net.psforever.objects.avatar._
@ -46,7 +47,8 @@ import net.psforever.objects.vital.base._
import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
import net.psforever.objects.zones._
import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity}
import net.psforever.packet._
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.game.objectcreate._
@ -2101,6 +2103,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
sendResponse(ObjectDeleteMessage(kguid, 0))
case AvatarResponse.KitNotUsed(_, "") =>
kitToBeUsed = None
case AvatarResponse.KitNotUsed(_, msg) =>
kitToBeUsed = None
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", msg, None))
@ -2469,9 +2474,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, 0, 0, zang))
)
} else {
log.info(s"${player.Name} is prepped for dropping")
//get ready for orbital drop
DismountAction(tplayer, obj, seat_num)
log.info(s"${player.Name} is prepped for dropping")
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
continent.VehicleEvents ! VehicleServiceMessage(
player.Name,
@ -3748,6 +3754,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
) =>
persist()
turnCounterFunc(avatar_guid)
updateBlockMap(player, continent, pos)
val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || is_jumping || jump_thrust
if (isMovingPlus) {
@ -3827,7 +3834,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (player.death_by == -1) {
KickedByAdministration()
}
player.zoneInteraction()
player.zoneInteractions()
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
//the majority of the following check retrieves information to determine if we are in control of the child
@ -3883,6 +3890,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//we're driving the vehicle
persist()
turnCounterFunc(player.GUID)
if (obj.MountedIn.isEmpty) {
updateBlockMap(obj, continent, pos)
}
val seat = obj.Seats(0)
player.Position = pos //convenient
if (obj.WeaponControlledFromSeat(0).isEmpty) {
@ -3926,7 +3936,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
)
updateSquad()
obj.zoneInteraction()
obj.zoneInteractions()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
@ -5283,7 +5293,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
SpecialEmp.emp,
SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
SpecialEmp.findAllBoomers
SpecialEmp.findAllBoomers(profile.DamageRadius)
)
}
if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
@ -6910,7 +6920,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case _ =>
vehicle.MountedIn = None
}
vehicle.allowZoneEnvironmentInteractions = true
vehicle.allowInteraction = true
data
} else {
//passenger
@ -7747,7 +7757,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}
/**
* Common activities/procedure when a player dismounts a valid object.
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
@ -8236,7 +8246,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
}
//
vehicle.allowZoneEnvironmentInteractions = false
vehicle.allowInteraction = false
if (!zoneReload && zoneId == continent.id) {
if (vehicle.Definition == GlobalDefinitions.droppod) {
//instant action droppod in the same zone
@ -9149,6 +9159,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}
}
def updateBlockMap(target: BlockMapEntity, zone: Zone, newCoords: Vector3): Unit = {
target.blockMapEntry match {
case Some(entry) =>
if (BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.range).toSet.equals(entry.sectors)) {
target.updateBlockMapEntry(newCoords) //soft update
} else {
zone.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update
}
case None => ;
}
}
def failWithError(error: String) = {
log.error(error)
middlewareActor ! MiddlewareActor.Teardown()

View file

@ -7,6 +7,7 @@ import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
@ -53,6 +54,14 @@ object ZoneActor {
final case class DespawnVehicle(vehicle: Vehicle) extends Command
final case class AddToBlockMap(target: BlockMapEntity, toPosition: Vector3) extends Command
final case class UpdateBlockMap(target: BlockMapEntity, toPosition: Vector3) extends Command
final case class RemoveFromBlockMap(target: BlockMapEntity) extends Command
final case class ChangedSectors(addedTo: SectorGroup, removedFrom: SectorGroup)
final case class HotSpotActivity(defender: SourceEntry, attacker: SourceEntry, location: Vector3) extends Command
// TODO remove
@ -124,6 +133,15 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case DespawnVehicle(vehicle) =>
zone.Transport ! Zone.Vehicle.Despawn(vehicle)
case AddToBlockMap(target, toPosition) =>
zone.blockMap.addTo(target, toPosition)
case UpdateBlockMap(target, toPosition) =>
zone.blockMap.move(target, toPosition)
case RemoveFromBlockMap(target) =>
zone.blockMap.removeFrom(target)
case HotSpotActivity(defender, attacker, location) =>
zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location)

View file

@ -50,9 +50,8 @@ object BoomerDeployableDefinition {
class BoomerDeployableControl(mine: BoomerDeployable)
extends ExplosiveDeployableControl(mine) {
override def receive: Receive =
deployableBehavior
.orElse(takesDamage)
def receive: Receive =
commonMineBehavior
.orElse {
case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if mine.Trigger.contains(trigger) =>
// the trigger damages the mine, which sets it off, which causes an explosion

View file

@ -1,22 +1,24 @@
// Copyright (c) 2018 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
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
import net.psforever.objects.definition.{DeployableDefinition, ExoSuitDefinition}
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.geometry.Geometry3D
import net.psforever.objects.geometry.d3.VolumetricGeometry
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.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.Vector3
import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -30,7 +32,12 @@ class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition)
override def Definition: ExplosiveDeployableDefinition = cdef
}
class ExplosiveDeployableDefinition(private val objectId: Int) extends DeployableDefinition(objectId) {
object ExplosiveDeployable {
final case class TriggeredBy(obj: PlanetSideServerObject)
}
class ExplosiveDeployableDefinition(private val objectId: Int)
extends DeployableDefinition(objectId) {
Name = "explosive_deployable"
DeployCategory = DeployableCategory.Mines
Model = SimpleResolutions.calculate
@ -38,6 +45,8 @@ class ExplosiveDeployableDefinition(private val objectId: Int) extends Deployabl
private var detonateOnJamming: Boolean = true
var triggerRadius: Float = 0f
def DetonateOnJamming: Boolean = detonateOnJamming
def DetonateOnJamming_=(detonate: Boolean): Boolean = {
@ -47,7 +56,7 @@ class ExplosiveDeployableDefinition(private val objectId: Int) extends Deployabl
override def Initialize(obj: Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[ExplosiveDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
@ -57,7 +66,7 @@ object ExplosiveDeployableDefinition {
}
}
class ExplosiveDeployableControl(mine: ExplosiveDeployable)
abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable)
extends Actor
with DeployableBehavior
with Damageable {
@ -69,12 +78,9 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable)
deployableBehaviorPostStop()
}
def receive: Receive =
def commonMineBehavior: Receive =
deployableBehavior
.orElse(takesDamage)
.orElse {
case _ => ;
}
override protected def PerformDamage(
target: Target,
@ -224,7 +230,15 @@ object ExplosiveDeployableControl {
* @return `true`, if the target entities are near enough to each other;
* `false`, otherwise
*/
def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = {
def detectTarget(
g1: VolumetricGeometry,
up: Vector3
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
) : Boolean = {
val g2 = obj2.Definition.Geometry(obj2)
val dir = g2.center.asVector3 - g1.center.asVector3
//val scalar = Vector3.ScalarProjection(dir, up)
@ -238,3 +252,109 @@ object ExplosiveDeployableControl {
) <= maxDistance
}
}
class MineDeployableControl(mine: ExplosiveDeployable)
extends ExplosiveDeployableControl(mine) {
def receive: Receive =
commonMineBehavior
.orElse {
case ExplosiveDeployable.TriggeredBy(obj) =>
setTriggered(Some(obj), delay = 200)
case MineDeployableControl.Triggered() =>
explodes(testForTriggeringTarget(
mine,
mine.Definition.innateDamage.map { _.DamageRadius }.getOrElse(mine.Definition.triggerRadius)
))
case _ => ;
}
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
//initial triggering upon build
setTriggered(testForTriggeringTarget(mine, mine.Definition.triggerRadius), delay = 1000)
}
def testForTriggeringTarget(mine: ExplosiveDeployable, range: Float): Option[PlanetSideServerObject] = {
val position = mine.Position
val faction = mine.Faction
val range2 = range * range
val sector = mine.Zone.blockMap.sector(position, range)
(sector.livePlayerList ++ sector.vehicleList)
.find { thing => thing.Faction != faction && Vector3.DistanceSquared(thing.Position, position) < range2 }
}
def setTriggered(instigator: Option[PlanetSideServerObject], delay: Long): Unit = {
instigator match {
case Some(_) if isConstructed.contains(true) && setup.isCancelled =>
//re-use the setup timer here
import scala.concurrent.ExecutionContext.Implicits.global
setup = context.system.scheduler.scheduleOnce(delay milliseconds, self, MineDeployableControl.Triggered())
case _ => ;
}
}
def explodes(instigator: Option[PlanetSideServerObject]): Unit = {
instigator match {
case Some(_) =>
//explosion
mine.Destroyed = true
ExplosiveDeployableControl.DamageResolution(
mine,
DamageInteraction(
SourceEntry(mine),
MineDeployableControl.trippedMineReason(mine),
mine.Position
).calculate()(mine),
damage = 0
)
case None =>
//reset
setup = Default.Cancellable
}
}
}
object MineDeployableControl {
private case class Triggered()
def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = {
val deployableSource = DeployableSource(mine)
val blame = mine.OwnerName match {
case Some(name) =>
val(charId, exosuit, seated): (Long, ExoSuitType.Value, Boolean) = 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)
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)
}
val faction = mine.Faction
PlayerSource(
name,
charId,
GlobalDefinitions.avatar,
mine.Faction,
exosuit,
seated,
100,
100,
mine.Position,
Vector3.Zero,
None,
crouching = false,
jumping = false,
ExoSuitDefinition.Select(exosuit, faction)
)
case None =>
//credit where credit is due
deployableSource
}
TrippedMineReason(deployableSource, blame)
}
}

View file

@ -7082,6 +7082,7 @@ object GlobalDefinitions {
he_mine.DeployTime = Duration.create(1000, "ms")
he_mine.deployAnimation = DeployAnimation.Standard
he_mine.explodes = true
he_mine.triggerRadius = 3f
he_mine.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
@ -7105,6 +7106,39 @@ object GlobalDefinitions {
jammer_mine.DeployTime = Duration.create(1000, "ms")
jammer_mine.deployAnimation = DeployAnimation.Standard
jammer_mine.DetonateOnJamming = false
jammer_mine.explodes = true
jammer_mine.triggerRadius = 3f
jammer_mine.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.Splash
Damage0 = 0
DamageRadius = 10f
DamageAtEdge = 1.0f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
}
jammer_mine.Geometry = mine
spitfire_turret.Name = "spitfire_turret"

View file

@ -2,19 +2,20 @@
package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.ce.{Deployable, InteractWithMines}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.{ZoneAware, Zoning}
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
import net.psforever.types.{PlanetSideGUID, _}
import scala.annotation.tailrec
@ -22,7 +23,8 @@ import scala.util.{Success, Try}
class Player(var avatar: Avatar)
extends PlanetSideServerObject
with InteractsWithZoneEnvironment
with BlockMapEntity
with InteractsWithZone
with FactionAffinity
with Vitality
with ResistanceProfile
@ -30,6 +32,9 @@ class Player(var avatar: Avatar)
with JammableUnit
with ZoneAware
with AuraContainer {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 10))
private var backpack: Boolean = false
private var released: Boolean = false
private var armor: Int = 0

View file

@ -123,6 +123,7 @@ object SpecialEmp {
* A sort of `SpecialEmp` that only affects deployed boomer explosives
* must find affected deployed boomer explosive entities.
* @see `BoomerDeployable`
* @param range the distance to search for applicable sector
* @param zone the zone in which to search
* @param obj a game entity that is excluded from results
* @param properties information about the effect/damage
@ -131,10 +132,17 @@ object SpecialEmp {
* since only boomer explosives are returned, this second list can be ignored
*/
def findAllBoomers(
range: Float
)
(
zone: Zone,
obj: PlanetSideGameObject with FactionAffinity with Vitality,
properties: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
zone.DeployableList.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o }
zone
.blockMap
.sector(obj.Position, range)
.deployableList
.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o }
}
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.ce.InteractWithMines
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
@ -9,13 +10,15 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.concurrent.duration.FiniteDuration
@ -71,8 +74,9 @@ import scala.util.{Success, Try}
*/
class Vehicle(private val vehicleDef: VehicleDefinition)
extends AmenityOwner
with BlockMapEntity
with MountableWeapons
with InteractsWithZoneEnvironment
with InteractsWithZone
with Hackable
with FactionAffinity
with Deployment
@ -83,6 +87,9 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with CommonNtuContainer
with Container
with AuraContainer {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 30))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
private var decal: Int = 0

View file

@ -293,12 +293,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name,
AvatarAction.UseKit(kguid, kdef.ObjectId)
)
case None if msg.length > 0 =>
case _ =>
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.KitNotUsed(kit.GUID, msg)
)
case None => ;
}
case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) =>
@ -1217,7 +1216,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (player.Health > 0) {
StartAuraEffect(Aura.Fire, duration = 1250L) //burn
import scala.concurrent.ExecutionContext.Implicits.global
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(player, body, None))
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractingWithEnvironment(player, body, None))
}
}
}
@ -1258,7 +1257,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
interactionTimer = context.system.scheduler.scheduleOnce(
delay = 250 milliseconds,
self,
InteractWithEnvironment(player, body, None)
InteractingWithEnvironment(player, body, None)
)
case _ => ;
//something configured incorrectly; no need to keep checking

View file

@ -8,12 +8,14 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.ZoneAware
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.DeployableIcon
import net.psforever.types.PlanetSideEmpire
trait BaseDeployable
extends PlanetSideServerObject
with FactionAffinity
with BlockMapEntity
with Vitality
with OwnableByPlayer
with ZoneAware {

View file

@ -229,6 +229,7 @@ trait DeployableBehavior {
*/
def finalizeDeployable(callback: ActorRef): Unit = {
setup.cancel()
setup = Default.Cancellable
constructed = Some(true)
val obj = DeployableObject
val zone = obj.Zone
@ -267,6 +268,7 @@ trait DeployableBehavior {
val duration = time.getOrElse(Deployable.cleanup)
import scala.concurrent.ExecutionContext.Implicits.global
setup.cancel()
setup = Default.Cancellable
decay.cancel()
decay = context.system.scheduler.scheduleOnce(duration, self, DeployableBehavior.FinalizeElimination())
}
@ -288,6 +290,7 @@ trait DeployableBehavior {
*/
def dismissDeployable(): Unit = {
setup.cancel()
setup = Default.Cancellable
decay.cancel()
val obj = DeployableObject
val zone = obj.Zone

View file

@ -0,0 +1,53 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.ce
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction}
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable}
import net.psforever.types.PlanetSideGUID
/**
* This game entity may infrequently test whether it may interact with game world deployable extra-territorial munitions.
* "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion
* and maybe death.
*/
class InteractWithMines(range: Float)
extends ZoneInteraction {
/**
* mines that, though detected, are skipped from being alerted;
* in between interaction tests, a memory of the mines that were messaged last test are retained and
* are excluded from being messaged this test;
* mines that are detected a second time are cleared from the list and are available to be messaged in the next test
*/
private var skipTargets: List[PlanetSideGUID] = List()
/**
* Trample upon active mines in our current detection sector and alert those mines.
* @param target the fixed element in this test
*/
def interaction(target: InteractsWithZone): Unit = {
val faction = target.Faction
val targets = target.Zone.blockMap
.sector(target.Position, range)
.deployableList
.filter {
case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here
case ex: ExplosiveDeployable => ex.Faction != faction &&
Zone.distanceCheck(target, ex, ex.Definition.triggerRadius)
case _ => false
}
val notSkipped = targets.filterNot { t => skipTargets.contains(t.GUID) }
skipTargets = notSkipped.map { _.GUID }
notSkipped.foreach { t =>
t.Actor ! ExplosiveDeployable.TriggeredBy(target)
}
}
/**
* Mines can not be un-exploded or un-alerted.
* All that can be done is blanking our retained previous messaging targets.
* @param target the fixed element in this test
*/
def resetInteraction(target: InteractsWithZone): Unit = {
skipTargets = List()
}
}

View file

@ -3,7 +3,8 @@ package net.psforever.objects.definition
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter}
import net.psforever.objects.geometry.{Geometry3D, GeometryForm}
import net.psforever.objects.geometry.GeometryForm
import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.types.OxygenState
/**
@ -86,15 +87,15 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti
ServerSplashTargetsCentroid
}
private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint()
private var serverGeometry: Any => VolumetricGeometry = GeometryForm.representByPoint()
def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) {
def Geometry: Any => VolumetricGeometry = if (ServerSplashTargetsCentroid) {
GeometryForm.representByPoint()
} else {
serverGeometry
}
def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = {
def Geometry_=(func: Any => VolumetricGeometry): Any => VolumetricGeometry = {
serverGeometry = func
Geometry
}

View file

@ -5,6 +5,7 @@ import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.EquipmentDefinition
import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.PlanetSideEmpire
/**
@ -14,7 +15,10 @@ import net.psforever.types.PlanetSideEmpire
* and, special carried (like a lattice logic unit);
* and, dropped on the ground in the game world and render where it was deposited.
*/
abstract class Equipment extends PlanetSideGameObject with FactionAffinity {
abstract class Equipment
extends PlanetSideGameObject
with FactionAffinity
with BlockMapEntity {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def Faction: PlanetSideEmpire.Value = faction

View file

@ -0,0 +1,90 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import enumeratum.{Enum, EnumEntry}
import net.psforever.types.Vector3
/**
* For geometric entities that exist only in a given cardinal direction, using a plane of reference,
* the plane of reference that maps the values to a cordinate vector
* @see `Vector3D`
* @param direction the coordinate vector of "relative up"
*/
sealed abstract class AxisAlignment(direction: Vector3) extends EnumEntry {
/**
* Project one vector as a vector that can be represented in this coordinate axis.
* @param v the original vector
* @return the projected vector
*/
def asVector3(v: Vector3): Vector3
}
/**
* For geometric entities that exist in a two-dimensional context.
* @param direction the coordinate vector of "relative up"
*/
sealed abstract class AxisAlignment2D(direction: Vector3) extends AxisAlignment(direction) {
/**
* Project two values as a vector that can be represented in this coordinate axis.
* @param a the first value
* @param b the second value
* @return the projected vector
*/
def asVector3(a: Float, b: Float): Vector3
}
/**
* For geometric entities that exist in a three-dimensional context.
* More ceremonial, than anything else.
*/
sealed abstract class AxisAlignment3D extends AxisAlignment(Vector3.Zero) {
/**
* Project three values as a vector that can be represented in this coordinate axis.
* @param a the first value
* @param b the second value
* @param c the third value
* @return the projected vector
*/
def asVector3(a: Float, b: Float, c: Float): Vector3
}
object AxisAlignment extends Enum[AxisAlignment] {
val values: IndexedSeq[AxisAlignment] = findValues
/**
* Geometric entities in the XY-axis.
* Coordinates are x- and y-; up is the z-axis.
*/
case object XY extends AxisAlignment2D(Vector3(0,0,1)) {
def asVector3(v: Vector3): Vector3 = v.xy
def asVector3(a: Float, b: Float): Vector3 = Vector3(a,b,0)
}
/**
* Geometric entities in the YZ-axis.
* Coordinates are y- and z-; up is the x-axis.
*/
case object YZ extends AxisAlignment2D(Vector3(1,0,0)) {
def asVector3(v: Vector3): Vector3 = Vector3(0,v.y,v.z)
def asVector3(a: Float, b: Float): Vector3 = Vector3(0,a,b)
}
/**
* Geometric entities in the XZ-axis.
* Coordinates are x- and z-; up is the y-axis.
*/
case object XZ extends AxisAlignment2D(Vector3(0,1,0)) {
def asVector3(v: Vector3): Vector3 = Vector3(v.x,0,v.z)
def asVector3(a: Float, b: Float): Vector3 = Vector3(a,0,b)
}
/**
* For geometric entities that exist in a three-dimensional context.
* More ceremonial, than anything else.
*/
case object Free extends AxisAlignment3D() {
def asVector3(v: Vector3): Vector3 = v
def asVector3(a: Float, b: Float, c: Float): Vector3 = Vector3(a,b,c)
}
}

View file

@ -3,28 +3,73 @@ package net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* Calculation support for the geometric code.
*/
object Geometry {
/**
* Are two `Float` numbers equal enough to be considered equal?
* @param value1 the first value
* @param value2 the second value
* @param off how far the number can be inequal from each other
* @return `true`, if the two `Float` numbers are close enough to be considered equal;
* `false`, otherwise
*/
def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = {
val diff = value1 - value2
if (diff >= 0) diff <= off else diff > -off
}
/**
* Are two `Vector3` entities equal enough to be considered equal?
* @see `equalFloats`
* @param value1 the first coordinate triple
* @param value2 the second coordinate triple
* @param off how far any individual coordinate can be inequal from each other
* @return `true`, if the two `Vector3` entities are close enough to be considered equal;
* `false`, otherwise
*/
def equalVectors(value1: Vector3, value2: Vector3, off: Float = 0.001f): Boolean = {
equalFloats(value1.x, value2.x, off) &&
equalFloats(value1.y, value2.y, off) &&
equalFloats(value1.z, value2.z, off)
}
/**
* Are two `Vector3` entities equal enough to be considered equal?
* @see `equalFloats`
* @param value1 the first coordinate triple
* @param value2 the second coordinate triple
* @param off how far each individual coordinate can be inequal from the other
* @return `true`, if the two `Vector3` entities are close enough to be considered equal;
* `false`, otherwise
*/
def equalVectors(value1: Vector3, value2: Vector3, off: Vector3): Boolean = {
equalFloats(value1.x, value2.x, off.x) &&
equalFloats(value1.y, value2.y, off.y) &&
equalFloats(value1.z, value2.z, off.z)
}
/**
* Is the value close enough to be zero to be equivalently replaceable with zero?
* @see `math.abs`
* @see `math.signum`
* @see `math.ulp`
* @see `Vector3.closeToInsignificance`
* @param d the original number
* @param epsilon how far from zero the value is allowed to stray
* @return the original number, or zero
*/
def closeToInsignificance(d: Float, epsilon: Float = 10f): Float = {
val ulp = math.ulp(epsilon)
math.signum(d) match {
case -1f =>
val n = math.abs(d)
val p = math.abs(n - n.toInt)
if (p < ulp || d > ulp) d + p else d
if (p < ulp || d > ulp) 0f else d
case _ =>
val p = math.abs(d - d.toInt)
if (p < ulp || d < ulp) d - p else d
if (p < ulp || d < ulp) 0f else d
}
}
}

View file

@ -2,12 +2,13 @@
package net.psforever.objects.geometry
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.geometry.d3._
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player}
import net.psforever.types.{ExoSuitType, Vector3}
object GeometryForm {
/** this point can not be used for purposes of geometric representation */
lazy val invalidPoint: Point3D = Point3D(Float.MinValue, Float.MinValue, Float.MinValue)
lazy val invalidPoint: d3.Point = d3.Point(Float.MinValue, Float.MinValue, Float.MinValue)
/** this cylinder can not be used for purposes of geometric representation */
lazy val invalidCylinder: Cylinder = Cylinder(invalidPoint.asVector3, Vector3.Zero, Float.MinValue, 0)
@ -16,22 +17,22 @@ object GeometryForm {
* @param o the entity
* @return the representation
*/
def representByPoint()(o: Any): Geometry3D = {
def representByPoint()(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject => Point3D(p.Position)
case s: SourceEntry => Point3D(s.Position)
case p: PlanetSideGameObject => Point(p.Position)
case s: SourceEntry => Point(s.Position)
case _ => invalidPoint
}
}
/**
* The geometric representation is a sphere around the entity's centroid
* positioned following the axis of rotation (the entity's base).
* @param radius how wide a hemisphere is
* The geometric representation is a sphere using a position as the entity's centroid
* and all points exist a distance from that point.
* @param radius how wide a quarter-sphere is
* @param o the entity
* @return the representation
*/
def representBySphere(radius: Float)(o: Any): Geometry3D = {
def representBySphere(radius: Float)(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject =>
Sphere(p.Position, radius)
@ -42,6 +43,25 @@ object GeometryForm {
}
}
/**
* The geometric representation is a sphere using a position as the entity's base (foot position)
* and the centroid is located just above it a fixed distance.
* All points exist a distance from that centroid.
* @param radius how wide a quarter-sphere is
* @param o the entity
* @return the representation
*/
def representBySphereOnBase(radius: Float)(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject =>
Sphere(p.Position + Vector3.z(radius), radius)
case s: SourceEntry =>
Sphere(s.Position + Vector3.z(radius), radius)
case _ =>
Sphere(invalidPoint, radius)
}
}
/**
* The geometric representation is a sphere around the entity's centroid
* positioned following the axis of rotation (the entity's base).
@ -49,7 +69,7 @@ object GeometryForm {
* @param o the entity
* @return the representation
*/
def representByRaisedSphere(radius: Float)(o: Any): Geometry3D = {
def representByRaisedSphere(radius: Float)(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject =>
Sphere(p.Position + Vector3.relativeUp(p.Orientation) * radius, radius)
@ -67,7 +87,7 @@ object GeometryForm {
* @param o the entity
* @return the representation
*/
def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = {
def representByCylinder(radius: Float, height: Float)(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject => Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height)
case s: SourceEntry => Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height)
@ -82,7 +102,7 @@ object GeometryForm {
* @param o the entity
* @return the representation
*/
def representPlayerByCylinder(radius: Float)(o: Any): Geometry3D = {
def representPlayerByCylinder(radius: Float)(o: Any): VolumetricGeometry = {
o match {
case p: Player =>
val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f
@ -113,7 +133,7 @@ object GeometryForm {
* @param o the entity
* @return the representation
*/
def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): Geometry3D = {
def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): VolumetricGeometry = {
o match {
case p: PlanetSideGameObject =>
Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height)

View file

@ -0,0 +1,102 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* Basic interface for all geometry.
*/
trait PrimitiveGeometry {
/**
* The centroid of the geometry.
* @return a point
*/
def center: Point
/**
* Move the centroid of the shape to the given point
* @param point the new center point
* @return geometry centered on the new point;
* ideally, should be the same type of geometry as the original object
*/
def moveCenter(point: Point): PrimitiveGeometry
}
/**
* Characteristics of a geometric figure with only three coordinates to define a position.
*/
trait Point {
/**
* Transform the point into the common interchangeable format for coordinates.
* They're very similar, anyway.
* @return a `Vector3` entity of the same denomination
*/
def asVector3: Vector3
}
/**
* Characteristics of a geometric figure defining a direction or a progressive change in coordinates.
*/
trait Slope {
/**
* The slope itself.
* @return a `Vector3` entity
*/
def d: Vector3
/**
* How long the slope goes on for.
* @return The length of the slope
*/
def length: Float
}
object Slope {
/**
* On occasions, the defined slope should have a length of one unit.
* It is a unit vector.
* @param v the input slope as a `Vector3` entity
* @throws `AssertionError` if the length is more or less than 1.
*/
def assertUnitVector(v: Vector3): Unit = {
assert({
val mag = Vector3.Magnitude(v)
mag - 0.05f < 1f && mag + 0.05f > 1f
}, "not a unit vector")
}
}
/**
* Characteristics of a geometric figure indicating an infinite slope - a mathematical line.
* The slope is always a unit vector.
* The point that assists to define the line is a constraint that the line must pass through.
*/
trait Line extends Slope {
Slope.assertUnitVector(d)
def p: Point
/**
* The length of a mathematical line is infinite.
* @return The length of the slope
*/
def length: Float = Float.PositiveInfinity
}
/**
* Characteristics of a geometric figure that have two endpoints, defining a fixed-length slope.
*/
trait Segment extends Slope {
/** The first point, considered the "start". */
def p1: Point
/** The second point, considered the "end". */
def p2: Point
def length: Float = Vector3.Magnitude(d)
/**
* Transform the segment into a matheatical line of the same slope.
* @return
*/
def asLine: PrimitiveGeometry
}

View file

@ -1,503 +0,0 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* Basic interface for all geometry.
*/
trait PrimitiveGeometry {
/**
* The centroid of the geometry.
* @return a point
*/
def center: Point
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* What counts as "the exterior" is limited to the complexity of the geometry.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
def pointOnOutside(v: Vector3) : Point
}
//trait Geometry2D extends PrimitiveGeometry {
// def center: Point2D
//
// def pointOnOutside(v: Vector3): Point2D = center
//}
/**
* Basic interface of all three-dimensional geometry.
* For the only real requirement for a hree-dimensional geometric figure is that it has three components of position
* and an equal number of components demonstrating equal that said dimensionality.
*/
trait Geometry3D extends PrimitiveGeometry {
def center: Point3D
def pointOnOutside(v: Vector3): Point3D = center
}
/**
* Characteristics of a geometric figure with only three coordinates to define a position.
*/
trait Point {
/**
* Transform the point into the common interchangeable format for coordinates.
* They're very similar, anyway.
* @return a `Vector3` entity of the same denomination
*/
def asVector3: Vector3
}
/**
* Characteristics of a geometric figure defining a direction or a progressive change in coordinates.
*/
trait Slope {
/**
* The slope itself.
* @return a `Vector3` entity
*/
def d: Vector3
/**
* How long the slope goes on for.
* @return The length of the slope
*/
def length: Float
}
object Slope {
/**
* On occasions, the defined slope should have a length of one unit.
* It is a unit vector.
* @param v the input slope as a `Vector3` entity
* @throws `AssertionError` if the length is more or less than 1.
*/
def assertUnitVector(v: Vector3): Unit = {
assert({
val mag = Vector3.Magnitude(v)
mag - 0.05f < 1f && mag + 0.05f > 1f
}, "not a unit vector")
}
}
/**
* Characteristics of a geometric figure indicating an infinite slope - a mathematical line.
* The slope is always a unit vector.
* The point that assists to define the line is a constraint that the line must pass through.
*/
trait Line extends Slope {
Slope.assertUnitVector(d)
def p: Point
/**
* The length of a mathematical line is infinite.
* @return The length of the slope
*/
def length: Float = Float.PositiveInfinity
}
/**
* Characteristics of a geometric figure that have two endpoints, defining a fixed-length slope.
*/
trait Segment extends Slope {
/** The first point, considered the "start". */
def p1: Point
/** The second point, considered the "end". */
def p2: Point
def length: Float = Vector3.Magnitude(d)
/**
* Transform the segment into a matheatical line of the same slope.
* @return
*/
def asLine: PrimitiveGeometry
}
/**
* The instance of a geometric coordinate position.
* @see `Vector3`
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
*/
final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point {
def center: Point3D = this
def asVector3: Vector3 = Vector3(x, y, z)
}
object Point3D {
/**
* An overloaded constructor that assigns world origin coordinates.
* @return a `Point3D` entity
*/
def apply(): Point3D = Point3D(0,0,0)
/**
* An overloaded constructor that uses the same coordinates from a `Vector3` entity.
* @param v the entity with the corresponding points
* @return a `Point3D` entity
*/
def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z)
}
/**
* The instance of a geometric coordinate position and a specific direction from that position.
* Rays are like mathematical lines in that they have infinite length;
* but, that infinite length is only expressed in a single direction,
* rather than proceeding in both a direction and its opposite direction from a target point.
* Infinity just be like that.
* Additionally, the point is not merely any point on the ray used to assist defining it
* and is instead considered the clearly-defined origin of the ray.
* @param p the point of origin
* @param d the direction
*/
final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line {
def center: Point3D = p
}
object Ray3D {
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Ray3D` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d)
/**
* An overloaded constructor that uses a `Vector3` entity to express coordinates.
* @param v the coordinates of the position
* @param d the direction
* @return a `Ray3D` entity
*/
def apply(v: Vector3, d: Vector3): Ray3D = Ray3D(Point3D(v.x, v.y, v.z), d)
}
/**
* The instance of a geometric coordinate position and a specific direction from that position.
* Mathematical lines have infinite length and their slope is represented as a unit vector.
* The point is merely a point used to assist in defining the line.
* @param p the point of origin
* @param d the direction
*/
final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line {
def center: Point3D = p
}
object Line3D {
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Line3D` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = {
Line3D(Point3D(x,y,z), d)
}
/**
* An overloaded constructor that uses a pair of individual coordinates
* and uses their difference to produce a unit vector to define a direction.
* @param ax the 'x' coordinate of the position
* @param ay the 'y' coordinate of the position
* @param az the 'z' coordinate of the position
* @param bx the 'x' coordinate of a destination position
* @param by the 'y' coordinate of a destination position
* @param bz the 'z' coordinate of a destination position
* @return a `Line3D` entity
*/
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = {
Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az)))
}
/**
* An overloaded constructor that uses a pair of points
* and uses their difference to produce a unit vector to define a direction.
* @param p1 the coordinates of the position
* @param p2 the coordinates of a destination position
* @return a `Line3D` entity
*/
def apply(p1: Point3D, p2: Point3D): Line3D = {
Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z)))
}
}
/**
* The instance of a limited span between two geometric coordinate positions, called "endpoints".
* Unlike mathematical lines, slope is treated the same as the vector leading from one point to the other
* and is the length of the segment.
* @param p1 a point
* @param p2 another point
*/
final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment {
/**
* The center point of a segment is a position that is equally in between both endpoints.
* @return a point
*/
def center: Point3D = Point3D((p2.asVector3 + p1.asVector3) * 0.5f)
def d: Vector3 = p2.asVector3 - p1.asVector3
def asLine: Line3D = Line3D(p1, Vector3.Unit(d))
}
object Segment3D {
/**
* An overloaded constructor that uses a pair of individual coordinates
* and uses their difference to define a direction.
* @param ax the 'x' coordinate of the position
* @param ay the 'y' coordinate of the position
* @param az the 'z' coordinate of the position
* @param bx the 'x' coordinate of a destination position
* @param by the 'y' coordinate of a destination position
* @param bz the 'z' coordinate of a destination position
* @return a `Segment3D` entity
*/
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = {
Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz))
}
/**
* An overloaded constructor.
* @param p the point of origin
* @param d the direction and distance (of the second point)
*/
def apply(p: Point3D, d: Vector3): Segment3D = {
Segment3D(p, Point3D(p.x + d.x, p.y + d.y, p.z + d.z))
}
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Segment3D` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = {
Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z))
}
}
/**
* The instance of a volumetric region that encapsulates all points within a certain distance of a central point.
* (That's what a sphere is.)
* A sphere has no real "top", "base", or "side" as all directions are described the same.
* @param p the point
* @param radius a distance that spans all points in any direction from the central point
*/
final case class Sphere(p: Point3D, radius: Float) extends Geometry3D {
def center: Point3D = p
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* All points that exist on the exterior of a sphere are on the surface of that sphere
* and are equally distant from the central point.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
override def pointOnOutside(v: Vector3): Point3D = {
val slope = Vector3.Unit(v)
val mult = radius / Vector3.Magnitude(slope)
Point3D(center.asVector3 + slope * mult)
}
}
object Sphere {
/**
* An overloaded constructor that only defines the radius of the sphere
* and places it at the world origin.
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(radius: Float): Sphere = Sphere(Point3D(), radius)
/**
* An overloaded constructor that uses individual coordinates to define the central point.
* * @param x the 'x' coordinate of the position
* * @param y the 'y' coordinate of the position
* * @param z the 'z' coordinate of the position
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius)
/**
* An overloaded constructor that uses vector coordinates to define the central point.
* @param v the coordinates of the position
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius)
}
/**
* The instance of a volumetric region that encapsulates all points within a certain distance of a central point.
* The region is characterized by a regular circular cross-section when observed from above or below
* and a flat top and a flat base when viewed from the side.
* The "base" is where the origin point is defined (at the center of a circular cross-section)
* and the "top" is discovered a `height` from the base along what the cylinder considers its `relativeUp` direction.
* @param p the point
* @param relativeUp what the cylinder considers its "up" direction
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
*/
final case class Cylinder(p: Point3D, relativeUp: Vector3, radius: Float, height: Float) extends Geometry3D {
Slope.assertUnitVector(relativeUp)
/**
* The center point of a cylinder is halfway between the "top" and the "base" along the direction of `relativeUp`.
* @return a point
*/
def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f)
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* A cylinder is composed of three clearly-defined regions on its exterior -
* two flat but circular surfaces that are the "top" and the "base"
* and a wrapped "sides" surface that defines all points connecting the "base" to the "top"
* along the `relativeUp` direction.
* The requested point may exist on any of these surfaces.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
override def pointOnOutside(v: Vector3): Point3D = {
val centerVector = center.asVector3
val slope = Vector3.Unit(v)
val dotProdOfSlopeAndUp = Vector3.DotProduct(slope, relativeUp)
if (Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = 1) || Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = -1)) {
// very rare condition: 'slope' and 'relativeUp' are parallel or antiparallel
Point3D(centerVector + slope * height * 0.5f)
} else {
val acrossTopAndBase = slope - relativeUp * dotProdOfSlopeAndUp
val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase))
val pointOnBase = p.asVector3 + acrossTopAndBase * radius
val pointOnTop = pointOnBase + relativeUp * height
val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide)
val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase)
val target = if(Geometry.equalVectors(fromPointOnTopToSide, Vector3.Zero) ||
Geometry.equalVectors(fromPointOnSideToBase, Vector3.Zero) ||
Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) {
//on side, including top rim or base rim
pointOnSide
} else {
//on top or base
// the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))'
// 'relativeUp` is already a unit vector (magnitude of 1)
centerVector + slope * height * 0.5f
}
Point3D(target)
}
}
}
object Cylinder {
/**
* An overloaded constructor where the 'relativeUp' of the cylinder is perpendicular to the xy-plane.
* @param p the point
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Point3D, radius: Float, height: Float): Cylinder = Cylinder(p, Vector3(0,0,1), radius, height)
/**
* An overloaded constructor where the origin point is expressed as a vector
* and the 'relativeUp' of the cylinder is perpendicular to the xy-plane.
* @param p the point
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), Vector3(0,0,1), radius, height)
/**
* An overloaded constructor the origin point is expressed as a vector.
* @param p the point
* @param v what the cylinder considers its "up" direction
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), v, radius, height)
}
/**
* Untested geometry.
* @param p na
* @param relativeForward na
* @param relativeUp na
* @param length na
* @param width na
* @param height na
*/
final case class Cuboid(
p: Point3D,
relativeForward: Vector3,
relativeUp: Vector3,
length: Float,
width: Float,
height: Float,
) extends Geometry3D {
def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f)
override def pointOnOutside(v: Vector3): Point3D = {
import net.psforever.types.Vector3.{CrossProduct, DotProduct, neg}
val height2 = height * 0.5f
val relativeSide = CrossProduct(relativeForward, relativeUp)
//val forwardVector = relativeForward * length
//val sideVector = relativeSide * width
//val upVector = relativeUp * height2
val closestVector: Vector3 = Seq(
relativeForward, relativeSide, relativeUp,
neg(relativeForward), neg(relativeSide), neg(relativeUp)
).maxBy { dir => DotProduct(dir, v) }
def dz(): Float = {
if (Geometry.closeToInsignificance(v.z) != 0) {
closestVector.z / v.z
} else {
0f
}
}
def dy(): Float = {
if (Geometry.closeToInsignificance(v.y) != 0) {
val fyfactor = closestVector.y / v.y
if (v.z * fyfactor <= height2) {
fyfactor
} else {
dz()
}
} else {
dz()
}
}
val scaleFactor: Float = {
if (Geometry.closeToInsignificance(v.x) != 0) {
val fxfactor = closestVector.x / v.x
if (v.y * fxfactor <= length) {
if (v.z * fxfactor <= height2) {
fxfactor
} else {
dy()
}
} else {
dy()
}
} else {
dy()
}
}
Point3D(center.asVector3 + (v * scaleFactor))
}
}

View file

@ -0,0 +1,18 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d2
import net.psforever.objects.geometry.{AxisAlignment2D, PrimitiveGeometry}
import net.psforever.types.Vector3
/**
* Basic interface of all two-dimensional geometry.
*/
trait Geometry2D extends PrimitiveGeometry {
def center: Point
def inPlane: AxisAlignment2D
def pointOnOutside(v: Vector3): Point = center
}
trait Flat extends Geometry2D

View file

@ -0,0 +1,76 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d2
import net.psforever.objects.geometry
import net.psforever.objects.geometry.{AxisAlignment, AxisAlignment2D}
import net.psforever.types.Vector3
/**
* The instance of a coordinate position in two-dimensional space.
* @see `Vector3`
* @param a the first coordinate of the position
* @param b the second coordinate of the position
* @param inPlane the planar orientation of "out", e.g., the XY-axis excludes z-coordinates;
* includes the identity of the coordinates as they are in-order
*/
final case class Point(a: Float, b: Float, inPlane: AxisAlignment2D)
extends Geometry2D
with geometry.Point {
def center: Point = this
def moveCenter(point: geometry.Point): Point = {
point match {
case p: Point if inPlane == p.inPlane => Point(p.a, p.b, inPlane)
case _ => this
}
}
def asVector3: Vector3 = inPlane.asVector3(a, b)
}
object Point {
/**
* An overloaded constructor that assigns world origin coordinates.
* By default, the planar frame is the common XY-axis.
* @return a `Point2D` entity
*/
def apply(): Point = Point(0,0, AxisAlignment.XY)
/**
* An overloaded constructor that assigns world origin coordinates.
* By default, the planar frame is the common XY-axis.
* @return a `Point2D` entity
*/
def apply(point: geometry.Point): Point = {
val p = point.asVector3
Point(p.x, p.y, AxisAlignment.XY)
}
/**
* An overloaded constructor that assigns world origin coordinates in the given planar frame.
* @return a `Point2D` entity
*/
def apply(frame: AxisAlignment2D): Point = Point(0,0, frame)
/**
* An overloaded constructor that uses the same coordinates from a `Vector3` entity.
* By default, the planar frame is the common XY-axis.
* @param v the entity with the corresponding points
* @return a `Point2D` entity
*/
def apply(v: Vector3): Point = Point(v.x, v.y, AxisAlignment.XY)
/**
* An overloaded constructor that uses the same coordinates from a `Vector3` entity in the given planar frame.
* By default, the planar frame is the common XY-axis.
* @param v the entity with the corresponding points
* @return a `Point2D` entity
*/
def apply(v: Vector3, frame: AxisAlignment2D): Point = {
frame match {
case AxisAlignment.XY => Point(v.x, v.y, frame)
case AxisAlignment.YZ => Point(v.y, v.z, frame)
case AxisAlignment.XZ => Point(v.x, v.z, frame)
}
}
}

View file

@ -0,0 +1,46 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d2
import net.psforever.objects.geometry
import net.psforever.objects.geometry.{AxisAlignment, AxisAlignment2D}
/**
* An axis-aligned planar region.
* @param top the highest "vertical" coordinate
* @param right the furthest "horizontal" coordinate
* @param base the highest "vertical" coordinate
* @param left the nearest "horizontal" coordinate
* @param inPlane the axis plan that the geometry occupies;
* for example, an XY-axis rectangle is Z-up
*/
final case class Rectangle(top: Float, right: Float, base: Float, left: Float, inPlane: AxisAlignment2D)
extends Flat {
assert(right > left, s"right needs to be greater than left - $right > $left")
assert(top > base, s"top needs to be greater than base - $top > $base")
def center: Point = Point((right + left) * 0.5f, (top + base) * 0.5f, inPlane)
def moveCenter(point: geometry.Point): Rectangle = {
point match {
case p: Point if inPlane == p.inPlane =>
val halfWidth = (right - left) * 0.5f
val halfHeight = (top - base) * 0.5f
Rectangle(p.b + halfHeight, p.a + halfWidth, p.b - halfHeight, p.a - halfWidth, inPlane)
case _ =>
this //TODO?
}
}
}
object Rectangle {
/**
* Overloaded constructor for a `Rectangle` in the XY-plane.
* @param top the highest "vertical" coordinate
* @param right the furthest "horizontal" coordinate
* @param base the highest "vertical" coordinate
* @param left the nearest "horizontal" coordinate
* @return a `Rectangle` entity
*/
def apply(top: Float, right: Float, base: Float, left: Float): Rectangle =
Rectangle(top, right, base, left, AxisAlignment.XY)
}

View file

@ -0,0 +1,78 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.objects.geometry.Geometry
import net.psforever.types.Vector3
/**
* Untested geometry.
* @param p na
* @param relativeForward na
* @param relativeUp na
* @param length na
* @param width na
* @param height na
*/
final case class Cuboid(
p: Point,
relativeForward: Vector3,
relativeUp: Vector3,
length: Float,
width: Float,
height: Float,
) extends VolumetricGeometry {
def center: Point = Point(p.asVector3 + relativeUp * height * 0.5f)
def moveCenter(point: geometry.Point): VolumetricGeometry = Cuboid(Point(point), relativeForward, relativeUp, length, width, height)
override def pointOnOutside(v: Vector3): Point = {
import net.psforever.types.Vector3.{CrossProduct, DotProduct, neg}
val height2 = height * 0.5f
val relativeSide = CrossProduct(relativeForward, relativeUp)
//val forwardVector = relativeForward * length
//val sideVector = relativeSide * width
//val upVector = relativeUp * height2
val closestVector: Vector3 = Seq(
relativeForward, relativeSide, relativeUp,
neg(relativeForward), neg(relativeSide), neg(relativeUp)
).maxBy { dir => DotProduct(dir, v) }
def dz(): Float = {
if (Geometry.closeToInsignificance(v.z) != 0) {
closestVector.z / v.z
} else {
0f
}
}
def dy(): Float = {
if (Geometry.closeToInsignificance(v.y) != 0) {
val fyfactor = closestVector.y / v.y
if (v.z * fyfactor <= height2) {
fyfactor
} else {
dz()
}
} else {
dz()
}
}
val scaleFactor: Float = {
if (Geometry.closeToInsignificance(v.x) != 0) {
val fxfactor = closestVector.x / v.x
if (v.y * fxfactor <= length) {
if (v.z * fxfactor <= height2) {
fxfactor
} else {
dy()
}
} else {
dy()
}
} else {
dy()
}
}
Point(center.asVector3 + (v * scaleFactor))
}
}

View file

@ -0,0 +1,100 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.objects.geometry.{Geometry, Slope}
import net.psforever.types.Vector3
/**
* The instance of a volumetric region that encapsulates all points within a certain distance of a central point.
* The region is characterized by a regular circular cross-section when observed from above or below
* and a flat top and a flat base when viewed from the side.
* The "base" is where the origin point is defined (at the center of a circular cross-section)
* and the "top" is discovered a `height` from the base along what the cylinder considers its `relativeUp` direction.
* @param p the point
* @param relativeUp what the cylinder considers its "up" direction
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
*/
final case class Cylinder(p: Point, relativeUp: Vector3, radius: Float, height: Float)
extends VolumetricGeometry {
Slope.assertUnitVector(relativeUp)
/**
* The center point of a cylinder is halfway between the "top" and the "base" along the direction of `relativeUp`.
* @return a point
*/
def center: Point = Point(p.asVector3 + relativeUp * height * 0.5f)
def moveCenter(point: geometry.Point): VolumetricGeometry = Cylinder(Point(point), relativeUp, radius, height)
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* A cylinder is composed of three clearly-defined regions on its exterior -
* two flat but circular surfaces that are the "top" and the "base"
* and a wrapped "sides" surface that defines all points connecting the "base" to the "top"
* along the `relativeUp` direction.
* The requested point may exist on any of these surfaces.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
override def pointOnOutside(v: Vector3): Point = {
val centerVector = center.asVector3
val slope = Vector3.Unit(v)
val dotProdOfSlopeAndUp = Vector3.DotProduct(slope, relativeUp)
if (Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = 1) || Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = -1)) {
// very rare condition: 'slope' and 'relativeUp' are parallel or antiparallel
Point(centerVector + slope * height * 0.5f)
} else {
val acrossTopAndBase = slope - relativeUp * dotProdOfSlopeAndUp
val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase))
val pointOnBase = p.asVector3 + acrossTopAndBase * radius
val pointOnTop = pointOnBase + relativeUp * height
val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide)
val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase)
val target = if(Geometry.equalVectors(fromPointOnTopToSide, Vector3.Zero) ||
Geometry.equalVectors(fromPointOnSideToBase, Vector3.Zero) ||
Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) {
//on side, including top rim or base rim
pointOnSide
} else {
//on top or base
// the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))'
// 'relativeUp` is already a unit vector (magnitude of 1)
centerVector + slope * height * 0.5f
}
Point(target)
}
}
}
object Cylinder {
/**
* An overloaded constructor where the 'relativeUp' of the cylinder is perpendicular to the xy-plane.
* @param p the point
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Point, radius: Float, height: Float): Cylinder = Cylinder(p, Vector3(0,0,1), radius, height)
/**
* An overloaded constructor where the origin point is expressed as a vector
* and the 'relativeUp' of the cylinder is perpendicular to the xy-plane.
* @param p the point
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point(p), Vector3(0,0,1), radius, height)
/**
* An overloaded constructor the origin point is expressed as a vector.
* @param p the point
* @param v what the cylinder considers its "up" direction
* @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction
* @param height the distance between the "base" and the "top"
* @return
*/
def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point(p), v, radius, height)
}

View file

@ -0,0 +1,28 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* Basic interface of all three-dimensional geometry.
* For the only real requirement for a three-dimensional geometric figure is that it has three components of position
* and an equal number of components demonstrating equal that said dimensionality.
*/
trait Geometry3D extends geometry.PrimitiveGeometry {
def center: Point
def moveCenter(point: geometry.Point): Geometry3D
}
trait VolumetricGeometry extends Geometry3D {
def moveCenter(point: geometry.Point): VolumetricGeometry
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* What counts as "the exterior" is limited to the complexity of the geometry.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
def pointOnOutside(v: Vector3): Point
}

View file

@ -0,0 +1,60 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* The instance of a geometric coordinate position and a specific direction from that position.
* Mathematical lines have infinite length and their slope is represented as a unit vector.
* The point is merely a point used to assist in defining the line.
* @param p the point of origin
* @param d the direction
*/
final case class Line(p: Point, d: Vector3)
extends Geometry3D
with geometry.Line {
def center: Point = p
def moveCenter(point: geometry.Point): Geometry3D = Line(Point(point), d)
}
object Line {
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Line` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Line = {
Line(Point(x,y,z), d)
}
/**
* An overloaded constructor that uses a pair of individual coordinates
* and uses their difference to produce a unit vector to define a direction.
* @param ax the 'x' coordinate of the position
* @param ay the 'y' coordinate of the position
* @param az the 'z' coordinate of the position
* @param bx the 'x' coordinate of a destination position
* @param by the 'y' coordinate of a destination position
* @param bz the 'z' coordinate of a destination position
* @return a `Line` entity
*/
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line = {
Line(Point(ax, ay, az), Vector3.Unit(Vector3(bx - ax, by - ay, bz - az)))
}
/**
* An overloaded constructor that uses a pair of points
* and uses their difference to produce a unit vector to define a direction.
* @param p1 the coordinates of the position
* @param p2 the coordinates of a destination position
* @return a `Line` entity
*/
def apply(p1: Point, p2: Point): Line = {
Line(p1, Vector3.Unit(Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)))
}
}

View file

@ -0,0 +1,43 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* The instance of a minute geometric coordinate position in three-dimensional space.
* The point is allowed to substitute for a sphere of zero radius, hence why it is volumetric
* (ignoring that a sphere of zero radius has no volume).
* @see `Vector3`
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
*/
final case class Point(x: Float, y: Float, z: Float)
extends VolumetricGeometry
with geometry.Point {
def center: Point = this
def moveCenter(point: geometry.Point): VolumetricGeometry = Point(point)
def asVector3: Vector3 = Vector3(x, y, z)
def pointOnOutside(v: Vector3): Point = center
}
object Point {
/**
* An overloaded constructor that assigns world origin coordinates.
* @return a `Point` entity
*/
def apply(): Point = Point(0,0,0)
def apply(point: geometry.Point): Point = Point(point.asVector3)
/**
* An overloaded constructor that uses the same coordinates from a `Vector3` entity.
* @param v the entity with the corresponding points
* @return a `Point` entity
*/
def apply(v: Vector3): Point = Point(v.x, v.y, v.z)
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* The instance of a geometric coordinate position and a specific direction from that position.
* Rays are like mathematical lines in that they have infinite length;
* but, that infinite length is only expressed in a single direction,
* rather than proceeding in both a direction and its opposite direction from a target point.
* Infinity just be like that.
* Additionally, the point is not merely any point on the ray used to assist defining it
* and is instead considered the clearly-defined origin of the ray.
* @param p the point of origin
* @param d the direction
*/
final case class Ray(p: Point, d: Vector3)
extends Geometry3D
with geometry.Line {
def center: Point = p
def moveCenter(point: geometry.Point): Geometry3D = Ray(Point(point), d)
}
object Ray {
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Ray` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Ray = Ray(Point(x,y,z), d)
/**
* An overloaded constructor that uses a `Vector3` entity to express coordinates.
* @param v the coordinates of the position
* @param d the direction
* @return a `Ray` entity
*/
def apply(v: Vector3, d: Vector3): Ray = Ray(Point(v.x, v.y, v.z), d)
}

View file

@ -0,0 +1,71 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* The instance of a limited span between two geometric coordinate positions, called "endpoints".
* Unlike mathematical lines, slope is treated the same as the vector leading from one point to the other
* and is the length of the segment.
* @param p1 a point
* @param p2 another point
*/
final case class Segment(p1: Point, p2: Point)
extends Geometry3D
with geometry.Segment {
/**
* The center point of a segment is a position that is equally in between both endpoints.
* @return a point
*/
def center: Point = Point((p2.asVector3 + p1.asVector3) * 0.5f)
def moveCenter(point: geometry.Point): Geometry3D = {
Segment(
Point(point.asVector3 - Vector3.Unit(d) * Vector3.Magnitude(d) * 0.5f),
d
)
}
def d: Vector3 = p2.asVector3 - p1.asVector3
def asLine: Line = Line(p1, Vector3.Unit(d))
}
object Segment {
/**
* An overloaded constructor that uses a pair of individual coordinates
* and uses their difference to define a direction.
* @param ax the 'x' coordinate of the position
* @param ay the 'y' coordinate of the position
* @param az the 'z' coordinate of the position
* @param bx the 'x' coordinate of a destination position
* @param by the 'y' coordinate of a destination position
* @param bz the 'z' coordinate of a destination position
* @return a `Segment` entity
*/
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment = {
Segment(Point(ax, ay, az), Point(bx, by, bz))
}
/**
* An overloaded constructor.
* @param p the point of origin
* @param d the direction and distance (of the second point)
*/
def apply(p: Point, d: Vector3): Segment = {
Segment(p, Point(p.x + d.x, p.y + d.y, p.z + d.z))
}
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position
* @param y the 'y' coordinate of the position
* @param z the 'z' coordinate of the position
* @param d the direction
* @return a `Segment` entity
*/
def apply(x: Float, y: Float, z: Float, d: Vector3): Segment = {
Segment(Point(x, y, z), Point(x + d.x, y + d.y, z + d.z))
}
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry.d3
import net.psforever.objects.geometry
import net.psforever.types.Vector3
/**
* The instance of a volumetric region that encapsulates all points within a certain distance of a central point.
* (That's what a sphere is.)
* When described by its center point, a sphere has no distinct "top", "base", or "side";
* all directions are described in the same way in reference to this center.
* It can be considered having a "base" and other "faces" for the purposes of settling on a surface (the ground).
* @param p the point
* @param radius a distance that spans all points in any direction from the central point
*/
final case class Sphere(p: Point, radius: Float)
extends VolumetricGeometry {
def center: Point = p
def moveCenter(point: geometry.Point): Sphere = Sphere(Point(point), radius)
/**
* Find a point on the exterior of the geometry if a line was drawn outwards from the centroid.
* All points that exist on the exterior of a sphere are on the surface of that sphere
* and are equally distant from the central point.
* @param v the vector in the direction of the point on the exterior
* @return a point
*/
override def pointOnOutside(v: Vector3): Point = {
val slope = Vector3.Unit(v)
val mult = radius / Vector3.Magnitude(slope)
Point(center.asVector3 + slope * mult)
}
}
object Sphere {
/**
* An overloaded constructor that only defines the radius of the sphere
* and places it at the world origin.
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(radius: Float): Sphere = Sphere(Point(), radius)
/**
* An overloaded constructor that uses individual coordinates to define the central point.
* * @param x the 'x' coordinate of the position
* * @param y the 'y' coordinate of the position
* * @param z the 'z' coordinate of the position
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point(x,y,z), radius)
/**
* An overloaded constructor that uses vector coordinates to define the central point.
* @param v the coordinates of the position
* @param radius a distance around the world origin coordinates
* @return a `Sphere` entity
*/
def apply(v: Vector3, radius: Float): Sphere = Sphere(Point(v), radius)
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.environment
import net.psforever.objects.geometry.d2.Rectangle
import net.psforever.types.Vector3
/**
@ -20,6 +21,8 @@ trait EnvironmentCollision {
* `false`, otherwise
*/
def testInteraction(pos: Vector3, varDepth: Float): Boolean
def bounding: Rectangle
}
/**
@ -32,6 +35,12 @@ final case class DeepPlane(altitude: Float)
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
pos.z + varDepth < altitude
}
def bounding: Rectangle = {
val max = Float.MaxValue * 0.25f
val min = Float.MinValue * 0.25f
Rectangle(max, max, min, min)
}
}
/**
@ -49,6 +58,8 @@ final case class DeepSquare(altitude: Float, north: Float, east: Float, south: F
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
pos.z + varDepth < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west
}
def bounding: Rectangle = Rectangle(north, east, south, west)
}
/**
@ -68,6 +79,8 @@ final case class DeepSurface(altitude: Float, north: Float, east: Float, south:
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
pos.z < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west
}
def bounding: Rectangle = Rectangle(north, east, south, west)
}
/**
@ -80,6 +93,8 @@ final case class DeepCircularSurface(center: Vector3, radius: Float)
extends EnvironmentCollision {
def altitude: Float = center.z
def bounding: Rectangle = Rectangle(center.y + radius, center.x + radius, center.y - radius, center.x - radius)
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
pos.z < center.z && Vector3.DistanceSquared(pos.xy, center.xy) < radius * radius
}

View file

@ -1,49 +1,168 @@
// Copyright (c) 2020 PSForever
// Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject.environment
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.{OxygenState, PlanetSideGUID}
import net.psforever.objects.zones._
import net.psforever.objects.zones.blockmap.BlockMapEntity
/**
* Related to the progress of interacting with a body of water deeper than you are tall or
* deeper than your vehicle is off the ground.
* @param guid the target
* @param state whether they are recovering or suffocating
* @param progress the percentage of completion towards the state
* This game entity may infrequently test whether it may interact with game world environment.
*/
final case class OxygenStateTarget(
guid: PlanetSideGUID,
state: OxygenState,
progress: Float
)
class InteractWithEnvironment()
extends ZoneInteraction {
private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any =
InteractWithEnvironment.onStableEnvironment()
/**
* The target has clipped into a critical region of a piece of environment.
* @param obj the target
* @param environment the terrain clipping region
* @param mountedVehicle whether or not the target is mounted
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
*/
final case class InteractWithEnvironment(
obj: PlanetSideServerObject,
environment: PieceOfEnvironment,
mountedVehicle: Option[OxygenStateTarget]
)
/**
* The method by which zone interactions are tested or a current interaction maintained.
* Utilize a function literal that, when called, returns a function literal of the same type;
* the function that is returned will not necessarily be the same as the one that was used
* but will represent the existing and ongoing status of interaction with the environment.
* Calling one function and exchanging it for another function to be called like this creates a procedure
* that controls and limits the interactions with the environment to only what is necessary.
* @see `InteractsWithEnvironment.blockedFromInteracting`
* @see `InteractsWithEnvironment.onStableEnvironment`
* @see `InteractsWithEnvironment.awaitOngoingInteraction`
*/
def interaction(target: InteractsWithZone): Unit = {
interactingWithEnvironment = interactingWithEnvironment(target, true)
.asInstanceOf[(PlanetSideServerObject, Boolean) => Any]
}
/**
* The target has ceased to clip into a critical region of a piece of environment.
* @param obj the target
* @param environment the previous terrain clipping region
* @param mountedVehicle whether or not the target is mounted
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
*/
final case class EscapeFromEnvironment(
obj: PlanetSideServerObject,
environment: PieceOfEnvironment,
mountedVehicle: Option[OxygenStateTarget]
)
/**
* Suspend any current interaction procedures through the proper channels
* or deactivate a previously flagged interaction blocking procedure
* and reset the system to its neutral state.
* The main difference between resetting and flagging the blocking procedure
* is that resetting will (probably) restore the previously active procedure on the next `zoneInteraction` call
* while blocking will halt all attempts to establish a new active interaction procedure
* and unblocking will immediately install whatever is the current active interaction.
* @see `InteractsWithEnvironment.onStableEnvironment`
*/
def resetInteraction(target: InteractsWithZone) : Unit = {
interactingWithEnvironment(target, false)
interactingWithEnvironment = InteractWithEnvironment.onStableEnvironment()
}
}
/**
* Completely reset any internal actions or processes related to environment clipping.
*/
final case class RecoveredFromEnvironmentInteraction()
object InteractWithEnvironment {
/**
* While on stable non-interactive terrain,
* test whether any special terrain component has an affect upon the target entity.
* If so, instruct the target that an interaction should occur.
* Considered tail recursive, but not treated that way.
* @see `blockedFromInteracting`
* @see `checkAllEnvironmentInteractions`
* @see `awaitOngoingInteraction`
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def onStableEnvironment()(obj: PlanetSideServerObject, allow: Boolean): Any = {
if(allow) {
checkAllEnvironmentInteractions(obj) match {
case Some(body) =>
obj.Actor ! InteractingWithEnvironment(obj, body, None)
awaitOngoingInteraction(obj.Zone, body)(_,_)
case None =>
onStableEnvironment()(_,_)
}
} else {
blockedFromInteracting()(_,_)
}
}
/**
* While on unstable, interactive, or special terrain,
* test whether that special terrain component has an affect upon the target entity.
* If no interaction exists,
* treat the target as if it had been previously affected by the given terrain,
* and instruct it to cease that assumption.
* Transition between the affects of different special terrains is possible.
* Considered tail recursive, but not treated that way.
* @see `blockedFromInteracting`
* @see `checkAllEnvironmentInteractions`
* @see `checkSpecificEnvironmentInteraction`
* @see `onStableEnvironment`
* @param zone the zone in which the terrain is located
* @param body the special terrain
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def awaitOngoingInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject, allow: Boolean): Any = {
if (allow) {
checkSpecificEnvironmentInteraction(zone, body)(obj) match {
case Some(_) =>
awaitOngoingInteraction(obj.Zone, body)(_, _)
case None =>
checkAllEnvironmentInteractions(obj) match {
case Some(newBody) if newBody.attribute == body.attribute =>
obj.Actor ! InteractingWithEnvironment(obj, newBody, None)
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
case Some(newBody) =>
obj.Actor ! EscapeFromEnvironment(obj, body, None)
obj.Actor ! InteractingWithEnvironment(obj, newBody, None)
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
case None =>
obj.Actor ! EscapeFromEnvironment(obj, body, None)
onStableEnvironment()(_, _)
}
}
} else {
obj.Actor ! EscapeFromEnvironment(obj, body, None)
blockedFromInteracting()(_,_)
}
}
/**
* Do not care whether on stable non-interactive terrain or on unstable interactive terrain.
* Wait until allowed to test again (external flag).
* Considered tail recursive, but not treated that way.
* @see `onStableEnvironment`
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def blockedFromInteracting()(obj: PlanetSideServerObject, allow: Boolean): Any = {
if (allow) {
onStableEnvironment()(obj, allow)
} else {
blockedFromInteracting()(_,_)
}
}
/**
* Test whether any special terrain component has an affect upon the target entity.
* @param obj the target entity
* @return any unstable, interactive, or special terrain that is being interacted
*/
def checkAllEnvironmentInteractions(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
val position = obj.Position
val depth = GlobalDefinitions.MaxDepth(obj)
(obj match {
case bme: BlockMapEntity =>
obj.Zone.blockMap.sector(bme).environmentList
case _ =>
obj.Zone.map.environment
}).find { body =>
body.attribute.canInteractWith(obj) && body.testInteraction(position, depth)
}
}
/**
* Test whether a special terrain component has an affect upon the target entity.
* @param zone the zone in which the terrain is located
* @param body the special terrain
* @param obj the target entity
* @return any unstable, interactive, or special terrain that is being interacted
*/
private def checkSpecificEnvironmentInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
if ((obj.Zone eq zone) && body.testInteraction(obj.Position, GlobalDefinitions.MaxDepth(obj))) {
Some(body)
} else {
None
}
}
}

View file

@ -0,0 +1,49 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.environment
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.{OxygenState, PlanetSideGUID}
/**
* Related to the progress of interacting with a body of water deeper than you are tall or
* deeper than your vehicle is off the ground.
* @param guid the target
* @param state whether they are recovering or suffocating
* @param progress the percentage of completion towards the state
*/
final case class OxygenStateTarget(
guid: PlanetSideGUID,
state: OxygenState,
progress: Float
)
/**
* The target has clipped into a critical region of a piece of environment.
* @param obj the target
* @param environment the terrain clipping region
* @param mountedVehicle whether or not the target is mounted
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
*/
final case class InteractingWithEnvironment(
obj: PlanetSideServerObject,
environment: PieceOfEnvironment,
mountedVehicle: Option[OxygenStateTarget]
)
/**
* The target has ceased to clip into a critical region of a piece of environment.
* @param obj the target
* @param environment the previous terrain clipping region
* @param mountedVehicle whether or not the target is mounted
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
*/
final case class EscapeFromEnvironment(
obj: PlanetSideServerObject,
environment: PieceOfEnvironment,
mountedVehicle: Option[OxygenStateTarget]
)
/**
* Completely reset any internal actions or processes related to environment clipping.
*/
final case class RecoveredFromEnvironmentInteraction()

View file

@ -1,185 +0,0 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.environment
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.Zone
/**
* This game entity may infrequently test whether it may interact with game world environment.
*/
trait InteractsWithZoneEnvironment {
_: PlanetSideServerObject =>
/** interactions for this particular entity is allowed */
private var _allowZoneEnvironmentInteractions: Boolean = true
/**
* If the environmental interactive permissions of this entity change.
*/
def allowZoneEnvironmentInteractions: Boolean = _allowZoneEnvironmentInteractions
/**
* If the environmental interactive permissions of this entity change,
* trigger a formal change to the interaction methodology.
* @param allow whether or not interaction is permitted
* @return whether or not interaction is permitted
*/
def allowZoneEnvironmentInteractions_=(allow: Boolean): Boolean = {
val before = _allowZoneEnvironmentInteractions
_allowZoneEnvironmentInteractions = allow
if (before != allow) {
zoneInteraction()
}
_allowZoneEnvironmentInteractions
}
private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any =
InteractsWithZoneEnvironment.onStableEnvironment()
/**
* The method by which zone interactions are tested or a current interaction maintained.
* Utilize a function literal that, when called, returns a function literal of the same type;
* the function that is returned will not necessarily be the same as the one that was used
* but will represent the existing and ongoing status of interaction with the environment.
* Calling one function and exchanging it for another function to be called like this creates a procedure
* that controls and limits the interactions with the environment to only what is necessary.
* @see `InteractsWithZoneEnvironment.blockedFromInteracting`
* @see `InteractsWithZoneEnvironment.onStableEnvironment`
* @see `InteractsWithZoneEnvironment.awaitOngoingInteraction`
*/
def zoneInteraction(): Unit = {
//val func: (PlanetSideServerObject, Boolean) => Any = interactingWithEnvironment(this, allowZoneEnvironmentInteractions)
interactingWithEnvironment = interactingWithEnvironment(this, allowZoneEnvironmentInteractions)
.asInstanceOf[(PlanetSideServerObject, Boolean) => Any]
}
/**
* Suspend any current interaction procedures through the proper channels
* or deactivate a previously flagged interaction blocking procedure
* and reset the system to its neutral state.
* The main difference between resetting and flagging the blocking procedure
* is that resetting will (probably) restore the previously active procedure on the next `zoneInteraction` call
* while blocking will halt all attempts to establish a new active interaction procedure
* and unblocking will immediately install whatever is the current active interaction.
* @see `InteractsWithZoneEnvironment.onStableEnvironment`
*/
def resetZoneInteraction() : Unit = {
_allowZoneEnvironmentInteractions = true
interactingWithEnvironment(this, false)
interactingWithEnvironment = InteractsWithZoneEnvironment.onStableEnvironment()
}
}
object InteractsWithZoneEnvironment {
/**
* While on stable non-interactive terrain,
* test whether any special terrain component has an affect upon the target entity.
* If so, instruct the target that an interaction should occur.
* Considered tail recursive, but not treated that way.
* @see `blockedFromInteracting`
* @see `checkAllEnvironmentInteractions`
* @see `awaitOngoingInteraction`
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def onStableEnvironment()(obj: PlanetSideServerObject, allow: Boolean): Any = {
if(allow) {
checkAllEnvironmentInteractions(obj) match {
case Some(body) =>
obj.Actor ! InteractWithEnvironment(obj, body, None)
awaitOngoingInteraction(obj.Zone, body)(_,_)
case None =>
onStableEnvironment()(_,_)
}
} else {
blockedFromInteracting()(_,_)
}
}
/**
* While on unstable, interactive, or special terrain,
* test whether that special terrain component has an affect upon the target entity.
* If no interaction exists,
* treat the target as if it had been previously affected by the given terrain,
* and instruct it to cease that assumption.
* Transition between the affects of different special terrains is possible.
* Considered tail recursive, but not treated that way.
* @see `blockedFromInteracting`
* @see `checkAllEnvironmentInteractions`
* @see `checkSpecificEnvironmentInteraction`
* @see `onStableEnvironment`
* @param zone the zone in which the terrain is located
* @param body the special terrain
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def awaitOngoingInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject, allow: Boolean): Any = {
if (allow) {
checkSpecificEnvironmentInteraction(zone, body)(obj) match {
case Some(_) =>
awaitOngoingInteraction(obj.Zone, body)(_, _)
case None =>
checkAllEnvironmentInteractions(obj) match {
case Some(newBody) if newBody.attribute == body.attribute =>
obj.Actor ! InteractWithEnvironment(obj, newBody, None)
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
case Some(newBody) =>
obj.Actor ! EscapeFromEnvironment(obj, body, None)
obj.Actor ! InteractWithEnvironment(obj, newBody, None)
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
case None =>
obj.Actor ! EscapeFromEnvironment(obj, body, None)
onStableEnvironment()(_, _)
}
}
} else {
obj.Actor ! EscapeFromEnvironment(obj, body, None)
blockedFromInteracting()(_,_)
}
}
/**
* Do not care whether on stable non-interactive terrain or on unstable interactive terrain.
* Wait until allowed to test again (external flag).
* Considered tail recursive, but not treated that way.
* @see `onStableEnvironment`
* @param obj the target entity
* @return the function literal that represents the next iterative call of ongoing interaction testing;
* may return itself
*/
def blockedFromInteracting()(obj: PlanetSideServerObject, allow: Boolean): Any = {
if (allow) {
onStableEnvironment()(obj, allow)
} else {
blockedFromInteracting()(_,_)
}
}
/**
* Test whether any special terrain component has an affect upon the target entity.
* @param obj the target entity
* @return any unstable, interactive, or special terrain that is being interacted
*/
def checkAllEnvironmentInteractions(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
val position = obj.Position
val depth = GlobalDefinitions.MaxDepth(obj)
obj.Zone.map.environment.find { body => body.attribute.canInteractWith(obj) && body.testInteraction(position, depth) }
}
/**
* Test whether a special terrain component has an affect upon the target entity.
* @param zone the zone in which the terrain is located
* @param body the special terrain
* @param obj the target entity
* @return any unstable, interactive, or special terrain that is being interacted
*/
private def checkSpecificEnvironmentInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
if ((obj.Zone eq zone) && body.testInteraction(obj.Position, GlobalDefinitions.MaxDepth(obj))) {
Some(body)
} else {
None
}
}
}

View file

@ -4,13 +4,15 @@ package net.psforever.objects.serverobject.environment
import enumeratum.{Enum, EnumEntry}
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.{PlanetSideGUID, Vector3}
/**
* The representation of a feature of the game world that is not a formal game object,
* usually terrain, but can be used to represent any bounded region.
*/
trait PieceOfEnvironment {
trait PieceOfEnvironment
extends BlockMapEntity {
/** a general description of this environment */
def attribute: EnvironmentTrait
/** a special representation of the region that qualifies as "this environment" */
@ -36,6 +38,18 @@ trait PieceOfEnvironment {
*/
def testStepIntoInteraction(pos: Vector3, previousPos: Vector3, varDepth: Float): Option[Boolean] =
PieceOfEnvironment.testStepIntoInteraction(body = this, pos, previousPos, varDepth)
def Position: Vector3 = collision.bounding.center.asVector3 + Vector3.z(collision.altitude)
def Position_=(vec : Vector3) : Vector3 = Position
def Orientation: Vector3 = Vector3.Zero
def Orientation_=(vec: Vector3): Vector3 = Vector3.Zero
def Velocity: Option[Vector3] = None
def Velocity_=(vec: Option[Vector3]): Option[Vector3] = None
}
/**
@ -100,6 +114,8 @@ final case class SeaLevel(attribute: EnvironmentTrait, altitude: Float)
private val planar = DeepPlane(altitude)
def collision : EnvironmentCollision = planar
override def Position: Vector3 = Vector3.Zero
}
object SeaLevel {

View file

@ -4,20 +4,21 @@ package net.psforever.objects.serverobject.environment
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.Default
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.types.OxygenState
import scala.collection.mutable
/**
* The mixin code for any server object that responds to the game world around it.
* The mixin code for any server object that responds to environmental representations in the game world.
* Specific types of environmental region is bound by geometry,
* designated by attributes,
* and gets reacted to when coming into contact with that geometry.
* and targets react when coming into contact with it.
* Ideally, the target under control instigates the responses towards the environment
* by independently re-evaluating the conditions of its interactions.
* Only one kind of environment can elicit a response at a time.
* While a reversal of this trigger scheme is possible, it is not ideal.
* @see `InteractsWithZoneEnvironment`
* @see `InteractsWithEnvironment`
* @see `PieceOfEnvironment`
*/
trait RespondsToZoneEnvironment {
@ -39,10 +40,10 @@ trait RespondsToZoneEnvironment {
private var interactWithEnvironmentStop: mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction] =
mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction]()
def InteractiveObject: PlanetSideServerObject with InteractsWithZoneEnvironment
def InteractiveObject: PlanetSideServerObject with InteractsWithZone
val environmentBehavior: Receive = {
case InteractWithEnvironment(target, body, optional) =>
case InteractingWithEnvironment(target, body, optional) =>
doEnvironmentInteracting(target, body, optional)
case EscapeFromEnvironment(target, body, optional) =>

View file

@ -2,6 +2,7 @@
package net.psforever.objects.serverobject.mount
import akka.actor.Actor
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.Player
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.serverobject.PlanetSideServerObject
@ -43,6 +44,7 @@ trait MountableBehavior {
case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) =>
user.VehicleSeated = obj.GUID
usedMountPoint.put(user.Name, mount_point)
obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(user)
sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
case _ =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point))
@ -84,6 +86,7 @@ trait MountableBehavior {
val obj = MountableObject
if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user)) {
user.VehicleSeated = None
obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position)
sender() ! Mountable.MountMessages(
user,
Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number))

View file

@ -6,6 +6,7 @@ import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageAndResistance
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.objects.zones.{Zone => World}
@ -23,7 +24,8 @@ import net.psforever.objects.zones.{Zone => World}
abstract class Amenity
extends PlanetSideServerObject
with Vitality
with StandardResistanceProfile {
with StandardResistanceProfile
with BlockMapEntity {
private[this] val log = org.log4s.getLogger("Amenity")
/** what other entity has authority over this amenity; usually either a building or a vehicle */

View file

@ -2,6 +2,7 @@
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}
@ -12,6 +13,7 @@ import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.BuildingInfoUpdateMessage
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
import scalax.collection.{Graph, GraphEdge}
@ -26,7 +28,8 @@ class Building(
private val zone: Zone,
private val buildingType: StructureType,
private val buildingDefinition: BuildingDefinition
) extends AmenityOwner {
) extends AmenityOwner
with BlockMapEntity {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var playersInSOI: List[Player] = List.empty

View file

@ -2,15 +2,11 @@
package net.psforever.objects.vehicles
import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.zones.Zone
import net.psforever.objects._
import net.psforever.objects.vehicles.CargoBehavior.{CheckCargoDismount, CheckCargoMounting}
import net.psforever.packet.game.{
CargoMountPointStatusMessage,
ObjectAttachMessage,
ObjectDetachMessage,
PlanetsideAttributeMessage
}
import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
import net.psforever.types.{CargoStatus, PlanetSideGUID, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.Service
@ -160,6 +156,7 @@ object CargoBehavior {
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
)
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
false
} else if (distance > 625 || iteration >= 40) {
//vehicles moved too far away or took too long to get into proper position; abort mounting
@ -289,6 +286,7 @@ object CargoBehavior {
hold.mount(cargo)
cargo.MountedIn = carrierGUID
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
false
} else {
//cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check
@ -391,6 +389,7 @@ object CargoBehavior {
s"$cargoActor",
VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
)
zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position)
if (carrier.isFlying) {
//the carrier vehicle is flying; eject the cargo vehicle
val ejectCargoMsg =

View file

@ -2,6 +2,7 @@
package net.psforever.objects.vehicles
import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.ce.TelepadLike
@ -406,6 +407,9 @@ class VehicleControl(vehicle: Vehicle)
case Some(player) =>
seat.unmount(player)
player.VehicleSeated = None
if (player.isAlive) {
zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position)
}
if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))
}
@ -703,7 +707,7 @@ class VehicleControl(vehicle: Vehicle)
/**
* Tell the given targets that
* water causes vehicles to become disabled if they dive off too far, too deep.
* @see `InteractWithEnvironment`
* @see `InteractingWithEnvironment`
* @see `OxygenState`
* @see `OxygenStateTarget`
* @param percentage the progress bar completion state
@ -717,7 +721,7 @@ class VehicleControl(vehicle: Vehicle)
): Unit = {
val vtarget = Some(OxygenStateTarget(vehicle.GUID, OxygenState.Suffocation, percentage))
targets.foreach { target =>
target.Actor ! InteractWithEnvironment(target, body, vtarget)
target.Actor ! InteractingWithEnvironment(target, body, vtarget)
}
}
@ -741,7 +745,7 @@ class VehicleControl(vehicle: Vehicle)
//keep doing damage
if (vehicle.Health > 0) {
import scala.concurrent.ExecutionContext.Implicits.global
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(obj, body, None))
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractingWithEnvironment(obj, body, None))
}
}
}
@ -787,7 +791,10 @@ class VehicleControl(vehicle: Vehicle)
percentage,
body,
vehicle.Seats.values
.flatMap { case seat if seat.isOccupied => seat.occupants }
.flatMap {
case seat if seat.isOccupied => seat.occupants
case _ => Nil
}
.filter { p => p.isAlive && (p.Zone eq vehicle.Zone) }
)
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.{DeployableSource, SourceEntry}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain an `ExplosiveDeployable` being detonated
* by incursion of an acceptable target into its automatic triggering range.
* @see `ExplosiveDeployable`
* @param mine na
* @param owner na
*/
final case class TrippedMineReason(mine: DeployableSource, owner: SourceEntry)
extends DamageReason {
def source: DamageProperties = mine.Definition.innateDamage.getOrElse(TrippedMineReason.triggered)
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = test match {
case trip: TrippedMineReason => mine == trip.mine && mine.OwnerName == trip.mine.OwnerName
case _ => false
}
/** lay the blame on the player who laid this mine, if possible */
def adversary: Option[SourceEntry] = Some(owner)
override def damageModel : DamageAndResistance = mine.Definition
override def attribution: Int = mine.Definition.ObjectId
}
object TrippedMineReason {
private val triggered = new DamageProperties {
Damage0 = 1 //token damage
SympatheticExplosion = true //sets off mine
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones
import net.psforever.objects.serverobject.PlanetSideServerObject
trait InteractsWithZone
extends PlanetSideServerObject {
/** interactions for this particular entity is allowed */
private var _allowInteraction: Boolean = true
/**
* If the interactive permissions of this entity change.
*/
def allowInteraction: Boolean = _allowInteraction
/**
* If the interactive permissions of this entity change,
* trigger a formal change to the interaction methodology.
* @param permit whether or not interaction is permitted
* @return whether or not interaction is permitted
*/
def allowInteraction_=(permit: Boolean): Boolean = {
val before = _allowInteraction
_allowInteraction = permit
if (before != permit) {
if (permit) {
interactions.foreach { _.interaction(target = this) }
} else {
interactions.foreach ( _.resetInteraction(target = this) )
}
}
_allowInteraction
}
private var interactions: List[ZoneInteraction] = List()
def interaction(func: ZoneInteraction): List[ZoneInteraction] = {
interactions = interactions :+ func
interactions
}
def interaction(): List[ZoneInteraction] = interactions
def zoneInteractions(): Unit = {
if (_allowInteraction) {
interactions.foreach { _.interaction(target = this) }
}
}
def resetInteractions(): Unit = {
interactions.foreach { _.resetInteraction(target = this) }
}
}
/**
* The basic behavior of an entity in a zone.
* @see `InteractsWithZone`
* @see `Zone`
*/
trait ZoneInteraction {
/**
* The method by which zone interactions are tested.
* How a target tests this interaction with elements of the target's zone.
* @param target the fixed element in this test
*/
def interaction(target: InteractsWithZone): Unit
/**
* Suspend any current interaction procedures.
* How the interactions are undone and stability restored to elements engaged with this target,
* even if only possible by small measure.
* Not all interactions can be reversed.
* @param target the fixed element in this test
*/
def resetInteraction(target: InteractsWithZone): Unit
}

View file

@ -106,7 +106,7 @@ case object MapInfo extends StringEnum[MapInfo] {
Pool(EnvironmentAttribute.Water, 43.765625f, 3997.2812f, 3991.539f, 3937.8906f, 3937.875f), //southwest of neit
Pool(EnvironmentAttribute.Water, 43.671875f, 2694.2031f, 3079.875f, 2552.414f, 2898.8203f), //west of anu
Pool(EnvironmentAttribute.Water, 42.671875f, 5174.4844f, 5930.133f, 4981.4297f, 5812.383f), //west of lugh
Pool(EnvironmentAttribute.Water, 42.203125f, 4935.742f, 5716.086f, 5496.6953f, 5444.5625f), //across road, west of lugh
Pool(EnvironmentAttribute.Water, 42.203125f, 4935.742f, 5716.086f, 4711.289f, 5444.5625f), //across road, west of lugh
Pool(EnvironmentAttribute.Water, 41.765625f, 2073.914f, 4982.5938f, 1995.4688f, 4899.086f), //L15-M16
Pool(EnvironmentAttribute.Water, 41.3125f, 3761.1484f, 2616.75f, 3627.4297f, 2505.1328f), //G11, south
Pool(EnvironmentAttribute.Water, 40.421875f, 4058.8281f, 2791.6562f, 3985.1016f, 2685.3672f) //G11, north

View file

@ -54,8 +54,10 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor {
def UpdateSOI(): Unit = {
sois.foreach {
case (facility, radius) =>
facility.PlayersInSOI =
zone.LivePlayers.filter(p => Vector3.DistanceSquared(facility.Position.xy, p.Position.xy) < radius)
val facilityXY = facility.Position.xy
facility.PlayersInSOI = zone.blockMap.sector(facility)
.livePlayerList
.filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius)
}
populateTick.cancel()
populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate())

View file

@ -38,7 +38,7 @@ import akka.actor.typed
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.geometry.Geometry3D
import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.doors.Door
@ -51,6 +51,7 @@ import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.blockmap.BlockMap
import net.psforever.services.Service
/**
@ -87,6 +88,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
/** The basic support structure for the globally unique number system used by this `Zone`. */
private var guid: NumberPoolHub = new NumberPoolHub(new MaxNumberSource(65536))
/** The blockmap structure for partitioning entities and environmental aspects of the zone.
* For a standard 8912`^`2 map, each of the four hundred formal map grids is 445.6m long and wide.
* A `desiredSpanSize` of 100m divides the blockmap into 8100 sectors.
* A `desiredSpanSize` of 50m divides the blockmap into 32041 sectors.
*/
val blockMap: BlockMap = BlockMap(map.scale, desiredSpanSize = 100)
/** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
private val equipmentOnGround: ListBuffer[Equipment] = ListBuffer[Equipment]()
@ -209,6 +217,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
MakeLattice()
AssignAmenities()
CreateSpawnGroups()
PopulateBlockMap()
validate()
}
@ -728,6 +737,15 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
entry
}
def PopulateBlockMap(): Unit = {
vehicles.foreach { vehicle => blockMap.addTo(vehicle) }
buildings.values.foreach { building =>
blockMap.addTo(building)
building.Amenities.foreach { amenity => blockMap.addTo(amenity) }
}
map.environment.foreach { env => blockMap.addTo(env) }
}
def StartPlayerManagementSystems(): Unit = {
soi ! SOI.Start()
}
@ -1163,14 +1181,8 @@ object Zone {
/**
* na
* @see `Amenity.Owner`
* @see `ComplexDeployable`
* @see `DamageWithPosition`
* @see `SimpleDeployable`
* @see `Zone.Buildings`
* @see `Zone.DeployableList`
* @see `Zone.LivePlayers`
* @see `Zone.Vehicles`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
* @param source a game entity that is treated as the origin and is excluded from results
* @param damagePropertiesBySource information about the effect/damage
@ -1183,30 +1195,17 @@ object Zone {
): List[PlanetSideServerObject with Vitality] = {
val sourcePosition = source.Position
val sourcePositionXY = sourcePosition.xy
val radius = damagePropertiesBySource.DamageRadius * damagePropertiesBySource.DamageRadius
val sectors = zone.blockMap.sector(sourcePositionXY, damagePropertiesBySource.DamageRadius)
//collect all targets that can be damaged
//players
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
val playerTargets = sectors.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
//vehicles
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
val vehicleTargets = sectors.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val deployableTargets = zone.DeployableList.filterNot { _.Destroyed }
val deployableTargets = sectors.deployableList.filterNot { _.Destroyed }
//amenities
val soiTargets = source match {
case o: Amenity =>
//fortunately, even where soi overlap, amenities in different buildings are never that close to each other
o.Owner.Amenities
case _ =>
zone.Buildings.values
.filter { b =>
val soiRadius = b.Definition.SOIRadius * b.Definition.SOIRadius
Vector3.DistanceSquared(sourcePositionXY, b.Position.xy) < soiRadius || soiRadius <= radius
}
.flatMap { _.Amenities }
.filter { _.Definition.Damageable }
}
//restrict to targets according to the detection plan
val soiTargets = sectors.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
//altogether ...
(playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets).filter { target => target ne source }
}
@ -1256,7 +1255,7 @@ object Zone {
* @return `true`, if the target entities are near enough to each other;
* `false`, otherwise
*/
private def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = {
private def distanceCheck(g1: VolumetricGeometry, g2: VolumetricGeometry, maxDistance: Float): Boolean = {
Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance ||
distanceCheck(g1, g2) <= maxDistance
}
@ -1270,7 +1269,7 @@ object Zone {
* @param g2 the geometric representation of a game entity
* @return the crude distance between the two geometric representations
*/
def distanceCheck(g1: Geometry3D, g2: Geometry3D): Float = {
def distanceCheck(g1: VolumetricGeometry, g2: VolumetricGeometry): Float = {
val dir = Vector3.Unit(g2.center.asVector3 - g1.center.asVector3)
val point1 = g1.pointOnOutside(dir).asVector3
val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3

View file

@ -3,6 +3,7 @@ package net.psforever.objects.zones
import akka.actor.Actor
import net.psforever.objects.Player
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.ce.Deployable
import scala.annotation.tailrec
@ -13,7 +14,6 @@ import scala.collection.mutable.ListBuffer
* @param zone the `Zone` object
*/
class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) extends Actor {
import ZoneDeployableActor._
private[this] val log = org.log4s.getLogger
@ -23,6 +23,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
if (DeployableBuild(obj, deployableList)) {
obj.Zone = zone
obj.Definition.Initialize(obj, context)
zone.actor ! ZoneActor.AddToBlockMap(obj, obj.Position)
obj.Actor ! Zone.Deployable.Setup()
} else {
log.warn(s"failed to build a ${obj.Definition.Name}")
@ -33,6 +34,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
if (DeployableBuild(obj, deployableList)) {
obj.Zone = zone
obj.Definition.Initialize(obj, context)
zone.actor ! ZoneActor.AddToBlockMap(obj, obj.Position)
owner.Actor ! Player.BuildDeployable(obj, tool)
} else {
log.warn(s"failed to build a ${obj.Definition.Name} belonging to ${obj.OwnerName.getOrElse("no one")}")
@ -43,6 +45,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
if (DeployableDismiss(obj, deployableList)) {
obj.Actor ! Zone.Deployable.IsDismissed(obj)
obj.Definition.Uninitialize(obj, context)
zone.actor ! ZoneActor.RemoveFromBlockMap(obj)
}
case Zone.Deployable.IsBuilt(_) => ;

View file

@ -2,6 +2,7 @@
package net.psforever.objects.zones
import akka.actor.Actor
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.equipment.Equipment
import net.psforever.types.PlanetSideGUID
import net.psforever.services.Service
@ -21,26 +22,28 @@ class ZoneGroundActor(zone: Zone, equipmentOnGround: ListBuffer[Equipment]) exte
def receive: Receive = {
case Zone.Ground.DropItem(item, pos, orient) =>
sender() ! (if (!item.HasGUID) {
Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
} else if (zone.GUID(item.GUID).isEmpty) {
Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone")
} else if (equipmentOnGround.contains(item)) {
Zone.Ground.CanNotDropItem(zone, item, "already dropped")
} else {
equipmentOnGround += item
item.Position = pos
item.Orientation = orient
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.DropItem(Service.defaultPlayerGUID, item)
)
Zone.Ground.ItemOnGround(item, pos, orient)
})
Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
} else if (zone.GUID(item.GUID).isEmpty) {
Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone")
} else if (equipmentOnGround.contains(item)) {
Zone.Ground.CanNotDropItem(zone, item, "already dropped")
} else {
equipmentOnGround += item
item.Position = pos
item.Orientation = orient
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.DropItem(Service.defaultPlayerGUID, item)
)
zone.actor ! ZoneActor.AddToBlockMap(item, pos)
Zone.Ground.ItemOnGround(item, pos, orient)
})
case Zone.Ground.PickupItem(item_guid) =>
sender() ! (FindItemOnGround(item_guid) match {
case Some(item) =>
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0))
zone.actor ! ZoneActor.RemoveFromBlockMap(item)
Zone.Ground.ItemInHand(item)
case None =>
Zone.Ground.CanNotPickupItem(zone, item_guid, "can not find")
@ -50,6 +53,7 @@ class ZoneGroundActor(zone: Zone, equipmentOnGround: ListBuffer[Equipment]) exte
//intentionally no callback
FindItemOnGround(item_guid) match {
case Some(item) =>
zone.actor ! ZoneActor.RemoveFromBlockMap(item)
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0))
case None => ;
}

View file

@ -2,8 +2,10 @@
package net.psforever.objects.zones
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
import net.psforever.objects.{Default, Player}
import scala.collection.concurrent.TrieMap
import scala.collection.mutable.ListBuffer
@ -34,6 +36,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
case player @ Some(tplayer) =>
tplayer.Zone = Zone.Nowhere
PlayerLeave(tplayer)
if (tplayer.VehicleSeated.isEmpty) {
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
}
sender() ! Zone.Population.PlayerHasLeft(zone, player)
if (playerMap.isEmpty) {
zone.StopPlayerManagementSystems()
@ -52,6 +57,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
name = GetPlayerControlName(player, None)
)
player.Zone = zone
if (player.VehicleSeated.isEmpty) {
zone.actor ! ZoneActor.AddToBlockMap(player, player.Position)
}
}
case None =>
sender() ! Zone.Population.PlayerCanNotSpawn(zone, player)
@ -61,6 +69,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
PopulationRelease(avatar.id, playerMap) match {
case Some(tplayer) =>
PlayerLeave(tplayer)
if (tplayer.VehicleSeated.isEmpty) {
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
}
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))
case None =>
sender() ! Zone.Population.PlayerHasLeft(zone, None)
@ -85,11 +96,13 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
name = s"corpse_of_${GetPlayerControlName(player, control)}"
)
player.Zone = zone
zone.actor ! ZoneActor.AddToBlockMap(player, player.Position)
}
case Zone.Corpse.Remove(player) =>
if (CorpseRemove(player, corpseList)) {
PlayerLeave(player)
zone.actor ! ZoneActor.RemoveFromBlockMap(player)
}
case _ => ;

View file

@ -2,6 +2,7 @@
package net.psforever.objects.zones
import akka.actor.{Actor, Props}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.{Default, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vehicles.VehicleControl
@ -44,6 +45,9 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
vehicle.Actor =
context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle))
}
if (vehicle.MountedIn.isEmpty) {
zone.actor ! ZoneActor.AddToBlockMap(vehicle, vehicle.Position)
}
sender() ! Zone.Vehicle.HasSpawned(zone, vehicle)
case Zone.Vehicle.Despawn(vehicle) =>
@ -52,6 +56,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
vehicleList.remove(index)
context.stop(vehicle.Actor)
vehicle.Actor = Default.Actor
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
case None => ;
sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find")

View file

@ -0,0 +1,397 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones.blockmap
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.zones.MapScale
import net.psforever.types.Vector3
import scala.collection.mutable.ListBuffer
/**
* A data structure which divides coordinate space into buckets or coordinate spans.
* The function of the blockmap is to organize the instantiated game objects (entities)
* that can be represented in coordinate space into a bucket each or into multiple buckets each
* that reflect their locality with other game objects in the same coordinate space.
* Polling based on either positions or on entities should be able to recover a lists of entities
* that are considered neighbors in the context of that position and a rectangular distance around the position.
* The purpose of the blockmap is to improve targeting when making such locality determinations.<br>
* <br>
* The coordinate space of a PlanetSide zone may contain 65535 entities, one of which is the same target entity.
* A bucket on the blockmap should contain only a small fraction of the full zone's entities.
* @param fullMapWidth maximum width of the coordinate space (m)
* @param fullMapHeight maximum height of the coordinate space (m)
* @param desiredSpanSize the amount of coordinate space attributed to each bucket in the blockmap (m)
*/
class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
/** a clamping of the desired span size to a realistic value to use for the span size;
* blocks can not be too small, but also should not be much larger than the width of the representable region
* a block spanning as wide as the map is an acceptable cap
*/
val spanSize: Int = math.min(math.max(10, desiredSpanSize), fullMapWidth)
/** how many sectors are in a row;
* the far side sector may run off into un-navigable regions but will always contain a sliver of represented map space,
* for example, on a 0-10 grid where the span size is 3, the spans will begin at (0, 3, 6, 9)
* and the last span will only have two-thirds of its region valid;
* the invalid, not represented regions should be silently ignored
*/
val blocksInRow: Int = fullMapWidth / spanSize + (if (fullMapWidth % spanSize > 0) 1 else 0)
/** the sectors / blocks / buckets into which entities that submit themselves are divided;
* while the represented region need not be square, the sectors are defined as squares
*/
val blocks: ListBuffer[Sector] = {
val horizontal: List[Int] = List.range(0, fullMapWidth, spanSize)
val vertical: List[Int] = List.range(0, fullMapHeight, spanSize)
ListBuffer.newBuilder[Sector].addAll(
vertical.flatMap { latitude =>
horizontal.map { longitude =>
new Sector(longitude, latitude, spanSize)
}
}
).result()
}
/**
* Given a blockmap entity,
* one that is allegedly represented on this blockmap,
* find the sector conglomerate in which this entity is allocated.
* @see `BlockMap.quickToSectorGroup`
* @param entity the target entity
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
*/
def sector(entity: BlockMapEntity): SectorPopulation = {
entity.blockMapEntry match {
case Some(entry) => BlockMap.quickToSectorGroup(entry.sectors.map { blocks })
case None => SectorGroup(Nil)
}
}
/**
* Given a coordinate position within representable space and a range from that representable space,
* find the sector conglomerate to which this range allocates.
* @see `BlockMap.findSectorIndices`
* @see `BlockMap.quickToSectorGroup`
* @param p the game world coordinates
* @param range the axis distance from the provided coordinates
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
*/
def sector(p: Vector3, range: Float): SectorPopulation = {
BlockMap.quickToSectorGroup( BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } )
}
/**
* Allocate this entity into appropriate sectors on the blockmap.
* @see `addTo(BlockMapEntity, Vector3)`
* @param target the entity
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def addTo(target: BlockMapEntity): SectorPopulation = {
addTo(target, target.Position)
}
/**
* Allocate this entity into appropriate sectors on the blockmap
* at the provided game world coordinates.
* @see `addTo(BlockMapEntity, Vector3, Float)`
* @see `BlockMap.rangeFromEntity`
* @param target the entity
* @param toPosition the custom game world coordinates that indicate the central sector
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def addTo(target: BlockMapEntity, toPosition: Vector3): SectorPopulation = {
addTo(target, toPosition, BlockMap.rangeFromEntity(target))
}
/**
* Allocate this entity into appropriate sectors on the blockmap
* using the provided custom axis range.
* @see `addTo(BlockMapEntity, Vector3, Float)`
* @param target the entity
* @param range the custom distance from the central sector along the major axes
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def addTo(target: BlockMapEntity, range: Float): SectorPopulation = {
addTo(target, target.Position, range)
}
/**
* Allocate this entity into appropriate sectors on the blockmap
* using the provided game world coordinates and the provided axis range.
* @see `BlockMap.findSectorIndices`
* @param target the entity
* @param toPosition the game world coordinates that indicate the central sector
* @param range the distance from the central sector along the major axes
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def addTo(target: BlockMapEntity, toPosition: Vector3, range: Float): SectorPopulation = {
val to = BlockMap.findSectorIndices(blockMap = this, toPosition, range)
val toSectors = to.toSet.map { blocks }
toSectors.foreach { block => block.addTo(target) }
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to.toSet))
BlockMap.quickToSectorGroup(toSectors)
}
/**
* Deallocate this entity from appropriate sectors on the blockmap.
* @see `actuallyRemoveFrom(BlockMapEntity, Vector3, Float)`
* @param target the entity
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def removeFrom(target: BlockMapEntity): SectorPopulation = {
target.blockMapEntry match {
case Some(entry) => actuallyRemoveFrom(target, entry.coords, entry.range)
case None => SectorGroup(Nil)
}
}
/**
* Deallocate this entity from appropriate sectors on the blockmap.
* Other parameters are included for symmetry with a respective `addto` method,
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
* as is not removing an entity from any sector that it occupies.
* @see `removeFrom(BlockMapEntity)`
* @param target the entity
* @param fromPosition ignored
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def removeFrom(target: BlockMapEntity, fromPosition: Vector3): SectorPopulation = {
removeFrom(target)
}
/**
* Deallocate this entity from appropriate sectors on the blockmap.
* Other parameters are included for symmetry with a respective `addto` method,
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
* as is not removing an entity from any sector that it occupies.
* @see `removeFrom(BlockMapEntity)`
* @param target the entity
* @param range ignored
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def removeFrom(target: BlockMapEntity, range: Float): SectorPopulation =
removeFrom(target)
/**
* Deallocate this entity from appropriate sectors on the blockmap.
* Other parameters are included for symmetry with a respective `addto` method,
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
* as is not removing an entity from any sector that it occupies.
* @see `removeFrom(BlockMapEntity)`
* @param target the entity
* @param fromPosition ignored
* @param range ignored
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def removeFrom(target: BlockMapEntity, fromPosition: Vector3, range: Float): SectorPopulation = {
removeFrom(target)
}
/**
* Deallocate this entity from appropriate sectors on the blockmap.
* Really.
* @param target the entity
* @param fromPosition the game world coordinates that indicate the central sector
* @param range the distance from the central sector along the major axes
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
private def actuallyRemoveFrom(target: BlockMapEntity, fromPosition: Vector3, range: Float): SectorPopulation = {
target.blockMapEntry match {
case Some(entry) =>
target.blockMapEntry = None
val from = entry.sectors.map { blocks }
from.foreach { block => block.removeFrom(target) }
BlockMap.quickToSectorGroup(from)
case None =>
SectorGroup(Nil)
}
}
/**
* Move an entity on the blockmap structure and update the prerequisite internal information.
* @see `move(BlockMapEntity, Vector3, Vector3, Float)`
* @param target the entity
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def move(target: BlockMapEntity): SectorPopulation = {
target.blockMapEntry match {
case Some(entry) => move(target, target.Position, entry.coords, entry.range)
case None => SectorGroup(Nil)
}
}
/**
* Move an entity on the blockmap structure and update the prerequisite internal information.
* @see `move(BlockMapEntity, Vector3, Vector3, Float)`
* @param target the entity
* @param toPosition the next location of the entity in world coordinates
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def move(target: BlockMapEntity, toPosition: Vector3): SectorPopulation = {
target.blockMapEntry match {
case Some(entry) => move(target, toPosition, entry.coords, entry.range)
case None => SectorGroup(Nil)
}
}
/**
* Move an entity on the blockmap structure and update the prerequisite internal information.
* @see `move(BlockMapEntity, Vector3)`
* @param target the entity
* @param toPosition the next location of the entity in world coordinates
* @param fromPosition ignored
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def move(target: BlockMapEntity, toPosition: Vector3, fromPosition: Vector3): SectorPopulation = {
move(target, toPosition)
}
/**
* Move an entity on the blockmap structure and update the prerequisite internal information.
* @param target the entity
* @param toPosition the next location of the entity in world coordinates
* @param fromPosition the current location of the entity in world coordinates
* @param range the distance from the location along the major axes
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def move(target: BlockMapEntity, toPosition: Vector3, fromPosition: Vector3, range: Float): SectorPopulation = {
target.blockMapEntry match {
case Some(entry) =>
val from = entry.sectors
val to = BlockMap.findSectorIndices(blockMap = this, toPosition, range).toSet
to.diff(from).foreach { index => blocks(index).addTo(target) }
from.diff(to).foreach { index => blocks(index).removeFrom(target) }
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to))
BlockMap.quickToSectorGroup(to.map { blocks })
case None =>
SectorGroup(Nil)
}
}
}
object BlockMap {
/**
* Overloaded constructor that uses a `MapScale` field, common with `Zone` entities.
* @param scale the two-dimensional scale of the map
* @param desiredSpanSize the length and width of a sector
* @return a ` BlockMap` entity
*/
def apply(scale: MapScale, desiredSpanSize: Int): BlockMap = {
new BlockMap(scale.width.toInt, scale.height.toInt, desiredSpanSize)
}
/**
* The blockmap is mapped to a coordinate range in two directions,
* so find the indices of the sectors that correspond to the region
* defined by the range around a coordinate position.
* @param blockMap the blockmap structure
* @param p the coordinate position
* @param range a rectangular range aigned with lateral axes extending from a coordinate position
* @return the indices of the sectors in the blockmap structure
*/
def findSectorIndices(blockMap: BlockMap, p: Vector3, range: Float): Iterable[Int] = {
findSectorIndices(blockMap.spanSize, blockMap.blocksInRow, blockMap.blocks.size, p, range)
}
/**
* The blockmap is mapped to a coordinate range in two directions,
* so find the indices of the sectors that correspond to the region
* defined by the range around a coordinate position.
* @param spanSize the length and width of a sector
* @param blocksInRow the number of sectors across the width (in a row) of the blockmap
* @param blocksTotal the number of sectors in the blockmap
* @param p the coordinate position
* @param range a rectangular range aigned with lateral axes extending from a coordinate position
* @return the indices of the sectors in the blockmap structure
*/
private def findSectorIndices(spanSize: Int, blocksInRow: Int, blocksTotal: Int, p: Vector3, range: Float): Iterable[Int] = {
val corners = {
/*
find the corners of a rectangular region extending in all cardinal directions from the position;
transform these corners into four sector indices;
if the first index matches the last index, the position and range are only in one sector;
[----][----][----]
[----][1234][----]
[----][----][----]
if the first and the second or the first and the third are further apart than an adjacent column or row,
then the missing indices need to be filled in and all of those sectors include the position and range;
[----][----][----][----][----]
[----][1 ][ ][2 ][----]
[----][ ][ ][ ][----]
[----][3 ][ ][4 ][----]
[----][----][----][----][----]
if neither of the previous, just return all distinct corner indices
[----][----][----][----] [----][----][----] [----][----][----][----]
[----][1 ][2 ][----] [----][1 2][----] [----][1 3][2 4][----]
[----][3 ][4 ][----] [----][3 4][----] [----][----][----][----]
[----][----][----][----] [----][----][----]
*/
val blocksInColumn = blocksTotal / blocksInRow
val lowx = math.max(0, p.x - range)
val highx = math.min(p.x + range, (blocksInRow * spanSize - 1).toFloat)
val lowy = math.max(0, p.y - range)
val highy = math.min(p.y + range, (blocksInColumn * spanSize - 1).toFloat)
Seq( (lowx, lowy), (highx, lowy), (lowx, highy), (highx, highy) )
}.map { case (x, y) =>
(y / spanSize).toInt * blocksInRow + (x / spanSize).toInt
}
if (corners.head == corners(3)) {
List(corners.head)
} else if (corners(1) - corners.head > 1 || corners(2) - corners.head > blocksInRow) {
(0 to (corners(2) - corners.head) / blocksInRow).flatMap { d =>
val perRow = d * blocksInRow
(corners.head + perRow) to (corners(1) + perRow)
}
} else {
corners.distinct
}
}
/**
* Calculate the range expressed by a certain entity that can be allocated into a sector on the blockmap.
* Entities have different ways of expressing these ranges.
* @param target the entity
* @param defaultRadius a default radius, if no specific case is discovered;
* if no default case, the default-default case is a single unit (`1.0f`)
* @return the distance from a central position along the major axes
*/
def rangeFromEntity(target: BlockMapEntity, defaultRadius: Option[Float] = None): Float = {
target match {
case b: Building =>
//use the building's sphere of influence
b.Definition.SOIRadius.toFloat// * 0.5f
case o: PlanetSideGameObject =>
//use the server geometry
val pos = target.Position
val v = o.Definition.Geometry(o)
math.sqrt(math.max(
Vector3.DistanceSquared(pos, v.pointOnOutside(Vector3(1,0,0)).asVector3),
Vector3.DistanceSquared(pos, v.pointOnOutside(Vector3(0,1,0)).asVector3)
)).toFloat
case e: PieceOfEnvironment =>
//use the bounds (like server geometry, but is alawys a rectangle on the XY-plane)
val bounds = e.collision.bounding
math.max(bounds.top - bounds.base, bounds.right - bounds.left) * 0.5f
case _ =>
//default and default-default
defaultRadius.getOrElse(1.0f)
}
}
/**
* If only one sector, just return that sector.
* If a group of sectors, organize them into a single referential sector.
* @param to all allocated sectors
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def quickToSectorGroup(to: Iterable[Sector]): SectorPopulation = {
if (to.size == 1) {
SectorGroup(to.head)
} else {
SectorGroup(to)
}
}
}

View file

@ -0,0 +1,92 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones.blockmap
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.zones.Zone
import net.psforever.types.Vector3
sealed case class BlockMapEntry(coords: Vector3, range: Float, sectors: Set[Int])
/**
* An game object that can be represented on a blockmap.
* The only requirement is that the entity can position itself in a zone's coordinate space.
* @see `BlockMap`
* @see `WorldEntity`
*/
trait BlockMapEntity
extends WorldEntity {
/** internal data regarding an active representation on a blockmap */
private var _blockMapEntry: Option[BlockMapEntry] = None
/** the function that allows for updates of the internal data */
private var _updateBlockMapEntryFunc: (BlockMapEntity, Vector3) => Boolean = BlockMapEntity.doNotUpdateBlockMap
/** internal data regarding an active representation on a blockmap */
def blockMapEntry: Option[BlockMapEntry] = _blockMapEntry
/** internal data regarding an active representation on a blockmap */
def blockMapEntry_=(entry: Option[BlockMapEntry]): Option[BlockMapEntry] = {
entry match {
case None =>
_updateBlockMapEntryFunc = BlockMapEntity.doNotUpdateBlockMap
_blockMapEntry = None
case Some(_) =>
_updateBlockMapEntryFunc = BlockMapEntity.updateBlockMap
_blockMapEntry = entry
}
entry
}
/**
* Buckets in the blockmap are called "sectors".
* Find the sectors in a given blockmap in which the entity would be represented within a given range.
* @param zone what region the blockmap represents
* @param range the custom distance from the central sector along the major axes
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def sector(zone: Zone, range: Float): SectorPopulation = {
zone.blockMap.sector(
//TODO same zone check?
_blockMapEntry match {
case Some(entry) => entry.coords
case None => Position
},
range
)
}
/**
* Update the internal data's known coordinate position without changing representation on whatever blockmap.
* Has the potential to cause major issues with the blockmap if used without external checks.
* @param newCoords the coordinate position
* @return `true`, if the coordinates were updated;
* `false`, otherwise
*/
def updateBlockMapEntry(newCoords: Vector3): Boolean = _updateBlockMapEntryFunc(this, newCoords)
}
object BlockMapEntity {
/**
* The entity is currently excluded from being represented on a blockmap structure.
* There is no need to update.
* @param target the entity on the blockmap
* @param newCoords the world coordinates of the entity, the position to which it is moving / being moved
* @return always `false`; we're not updating the entry
*/
private def doNotUpdateBlockMap(target: BlockMapEntity, newCoords: Vector3): Boolean = false
/**
* Re-using other data from the entry,
* update the data of the target entity's internal understanding of where it is represented on a blockmap.
* Act as if the sector and the range that is encompassed never change,
* though the game world coordinates of the entity have been changed.
* (The range would probably not have changed in any case.
* To properly update the range, perform a proper update.)
* @param target the entity on the blockmap
* @param newCoords the world coordinates of the entity, the position to which it is moving / being moved
* @return always `true`; we are updating this entry
*/
private def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Boolean = {
val oldEntry = target.blockMapEntry.get
target.blockMapEntry = Some(BlockMapEntry(newCoords, oldEntry.range, oldEntry.sectors))
true
}
}

View file

@ -0,0 +1,282 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones.blockmap
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.{Player, Vehicle}
import scala.collection.mutable.ListBuffer
/**
* The collections of entities in a sector conglomerate.
*/
trait SectorPopulation {
def livePlayerList: List[Player]
def corpseList: List[Player]
def vehicleList: List[Vehicle]
def equipmentOnGroundList: List[Equipment]
def deployableList: List[Deployable]
def buildingList: List[Building]
def amenityList: List[Amenity]
def environmentList: List[PieceOfEnvironment]
/**
* A count of all the entities in all the lists.
*/
def total: Int = {
livePlayerList.size +
corpseList.size +
vehicleList.size +
equipmentOnGroundList.size +
deployableList.size +
buildingList.size +
amenityList.size +
environmentList.size
}
}
/**
* Information about the sector.
*/
trait SectorTraits {
/** the starting coordinate of the region (in terms of width) */
def longitude: Float
/** the starting coordinate of the region (in terms of length) */
def latitude: Float
/** how width and long is the region */
def span: Int
}
/**
* Custom lists of entities for sector buckets.
* @param eqFunc a custom equivalence function to distinguish between the entities in the list
* @tparam A the type of object that will be the entities stored in the list
*/
class SectorListOf[A](eqFunc: (A, A) => Boolean = (a: A, b: A) => a equals b) {
private val internalList: ListBuffer[A] = ListBuffer[A]()
/**
* Insert the entity into the list as long as it cannot be found in the list
* according to the custom equivalence function.
* @param elem the entity
* @return a conventional list of entities
*/
def addTo(elem: A): List[A] = {
internalList.indexWhere { item => eqFunc(elem, item) } match {
case -1 => internalList.addOne(elem)
case _ => ;
}
list
}
/**
* Remove the entity from the list as long as it can be found in the list
* according to the custom equivalence function.
* @param elem the entity
* @return a conventional list of entities
*/
def removeFrom(elem: A): List[A] = {
internalList.indexWhere { item => eqFunc(elem, item) } match {
case -1 => ;
case index => internalList.remove(index)
}
list
}
/**
* Cast this specialized list of entities into a conventional list of entities.
* @return a conventional list of entities
*/
def list: List[A] = internalList.toList
}
/**
* The bucket of a blockmap structure
* that contains lists of entities that, within a given span of coordinate distance,
* are considered neighbors.
* While the coordinate space that supports a blockmap (?) may be any combination of two dimensions,
* the sectors are always square.
* @param longitude a starting coordinate of the region (in terms of width)
* @param latitude a starting coordinate of the region (in terms of length)
* @param span the distance across the sector in both directions
*/
class Sector(val longitude: Int, val latitude: Int, val span: Int)
extends SectorPopulation {
private val livePlayers: SectorListOf[Player] = new SectorListOf[Player](
(a: Player, b: Player) => a.CharId == b.CharId
)
private val corpses: SectorListOf[Player] = new SectorListOf[Player](
(a: Player, b: Player) => a.GUID == b.GUID || (a eq b)
)
private val vehicles: SectorListOf[Vehicle] = new SectorListOf[Vehicle](
(a: Vehicle, b: Vehicle) => a eq b
)
private val equipmentOnGround: SectorListOf[Equipment] = new SectorListOf[Equipment](
(a: Equipment, b: Equipment) => a eq b
)
private val deployables: SectorListOf[Deployable] = new SectorListOf[Deployable](
(a: Deployable, b: Deployable) => a eq b
)
private val buildings: SectorListOf[Building] = new SectorListOf[Building](
(a: Building, b: Building) => a eq b
)
private val amenities: SectorListOf[Amenity] = new SectorListOf[Amenity](
(a: Amenity, b: Amenity) => a eq b
)
private val environment: SectorListOf[PieceOfEnvironment] = new SectorListOf[PieceOfEnvironment](
(a: PieceOfEnvironment, b: PieceOfEnvironment) => a eq b
)
def livePlayerList : List[Player] = livePlayers.list
def corpseList: List[Player] = corpses.list
def vehicleList: List[Vehicle] = vehicles.list
def equipmentOnGroundList: List[Equipment] = equipmentOnGround.list
def deployableList: List[Deployable] = deployables.list
def buildingList: List[Building] = buildings.list
def amenityList : List[Amenity] = amenities.list
def environmentList: List[PieceOfEnvironment] = environment.list
/**
* Appropriate an entity added to this blockmap bucket
* inot a list of objects that are like itself.
* @param o the entity
* @return whether or not the entity was added
*/
def addTo(o: BlockMapEntity): Boolean = {
o match {
case p: Player =>
//players and corpses are the same kind of object, but are distinguished by a single flag
//when adding to the "corpse" list, first attempt to remove from the "player" list
if (!p.isBackpack) {
livePlayers.list.size < livePlayers.addTo(p).size
}
else {
livePlayers.removeFrom(p)
corpses.list.size < corpses.addTo(p).size
}
case v: Vehicle =>
vehicles.list.size < vehicles.addTo(v).size
case e: Equipment =>
equipmentOnGround.list.size < equipmentOnGround.addTo(e).size
case d: Deployable =>
deployables.list.size < deployables.addTo(d).size
case b: Building =>
buildings.list.size < buildings.addTo(b).size
case a: Amenity =>
amenities.list.size < amenities.addTo(a).size
case e: PieceOfEnvironment =>
environment.list.size < environment.addTo(e).size
case _ =>
false
}
}
/**
* Remove an entity added to this blockmap bucket
* from a list of already-added objects that are like itself.
* @param o the entity
* @return whether or not the entity was removed
*/
def removeFrom(o: Any): Boolean = {
o match {
case p: Player =>
livePlayers.list.size > livePlayers.removeFrom(p).size ||
corpses.list.size > corpses.removeFrom(p).size
case v: Vehicle =>
vehicles.list.size > vehicles.removeFrom(v).size
case e: Equipment =>
equipmentOnGround.list.size > equipmentOnGround.removeFrom(e).size
case d: Deployable =>
deployables.list.size > deployables.removeFrom(d).size
case _ =>
false
}
}
}
/**
* The specific datastructure that is mentioned when using the term "sector conglomerate".
* Typically used to compose the lists of entities from various individual sectors.
* @param livePlayerList the living players
* @param corpseList the dead players
* @param vehicleList vehicles
* @param equipmentOnGroundList dropped equipment
* @param deployableList deployed combat engineering gear
* @param buildingList the structures
* @param amenityList the structures within the structures
* @param environmentList fields that represent the game world environment
*/
class SectorGroup(
val livePlayerList: List[Player],
val corpseList: List[Player],
val vehicleList: List[Vehicle],
val equipmentOnGroundList: List[Equipment],
val deployableList: List[Deployable],
val buildingList: List[Building],
val amenityList: List[Amenity],
val environmentList: List[PieceOfEnvironment]
)
extends SectorPopulation
object SectorGroup {
/**
* Overloaded constructor that takes a single sector
* and transfers the lists of entities into a single conglomeration of the sector populations.
* @param sector the sector to be counted
* @return a `SectorGroup` object
*/
def apply(sector: Sector): SectorGroup = {
new SectorGroup(
sector.livePlayerList,
sector.corpseList,
sector.vehicleList,
sector.equipmentOnGroundList,
sector.deployableList,
sector.buildingList,
sector.amenityList,
sector.environmentList
)
}
/**
* Overloaded constructor that takes a group of sectors
* and condenses all of the lists of entities into a single conglomeration of the sector populations.
* @param sectors the series of sectors to be counted
* @return a `SectorGroup` object
*/
def apply(sectors: Iterable[Sector]): SectorGroup = {
new SectorGroup(
sectors.flatMap { _.livePlayerList }.toList.distinct,
sectors.flatMap { _.corpseList }.toList.distinct,
sectors.flatMap { _.vehicleList }.toList.distinct,
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,
sectors.flatMap { _.deployableList }.toList.distinct,
sectors.flatMap { _.buildingList }.toList.distinct,
sectors.flatMap { _.amenityList }.toList.distinct,
sectors.flatMap { _.environmentList }.toList.distinct
)
}
}

View file

@ -2923,18 +2923,20 @@ class SquadService extends Actor {
.toList
.unzip { case (member, index) => (member.CharId, index) }
val toChannel = features.ToChannel
memberCharIds.foreach { charId =>
SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad")
Publish(
charId,
SquadResponse.Join(
squad,
indices.filterNot(_ == position) :+ position,
toChannel
)
)
InitWaypoints(charId, squad.GUID)
}
memberCharIds
.map { id => (id, UserEvents.get(id)) }
.collect { case (id, Some(sub)) =>
SquadEvents.subscribe(sub, s"/$toChannel/Squad")
Publish(
id,
SquadResponse.Join(
squad,
indices.filterNot(_ == position) :+ position,
toChannel
)
)
InitWaypoints(id, squad.GUID)
}
//fully update for all users
InitSquadDetail(squad)
} else {

View file

@ -4,6 +4,8 @@ package objects
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import base.{ActorTest, FreedContextActorTest}
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.{Avatar, Certification, PlayerControl}
import net.psforever.objects.{ConstructionItem, Deployables, GlobalDefinitions, Player}
import net.psforever.objects.ce.{Deployable, DeployedItem}
@ -32,6 +34,8 @@ class DeployableBehaviorSetupTest extends ActorTest {
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(jmine, number = 1)
jmine.Faction = PlanetSideEmpire.TR
@ -91,6 +95,8 @@ class DeployableBehaviorSetupOwnedP1Test extends ActorTest {
override def Deployables: ActorRef = deployables
override def Players = List(avatar)
override def LivePlayers = List(player)
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)
@ -136,6 +142,8 @@ class DeployableBehaviorSetupOwnedP2Test extends FreedContextActorTest {
override def Players = List(avatar)
override def LivePlayers = List(player)
override def tasks: ActorRef = eventsProbe.ref
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)
@ -238,6 +246,8 @@ class DeployableBehaviorDeconstructTest extends ActorTest {
override def LocalEvents: ActorRef = eventsProbe.ref
override def tasks: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(jmine, number = 1)
jmine.Faction = PlanetSideEmpire.TR
@ -295,6 +305,8 @@ class DeployableBehaviorDeconstructOwnedTest extends FreedContextActorTest {
override def Players = List(avatar)
override def LivePlayers = List(player)
override def tasks: ActorRef = eventsProbe.ref
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(jmine, number = 1)
guid.register(citem, number = 2)

View file

@ -4,6 +4,7 @@ package objects
import akka.actor.{Actor, ActorRef, Props}
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.ballistics._
import net.psforever.objects.ce.DeployedItem
import net.psforever.objects.guid.NumberPoolHub
@ -23,6 +24,8 @@ import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import akka.actor.typed.scaladsl.adapter._
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration._
@ -339,7 +342,7 @@ class ExplosiveDeployableJammerTest extends ActorTest {
j_mine.Owner = player2
j_mine.OwnerName = player2.Name
j_mine.Faction = PlanetSideEmpire.NC
j_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], j_mine), "j-mine-control")
j_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], j_mine), "j-mine-control")
val jMineSource = SourceEntry(j_mine)
val pSource = PlayerSource(player1)
@ -406,6 +409,8 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
override def Players = List(avatar1, avatar2)
override def LivePlayers = List(player1, player2)
override def tasks: ActorRef = eventsProbe.ref
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
player1.Spawn()
player1.Actor = player1Probe.ref
@ -421,7 +426,9 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
h_mine.Owner = player2
h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control")
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
zone.blockMap.addTo(player1)
zone.blockMap.addTo(player2)
val pSource = PlayerSource(player1)
val projectile = weapon.Projectile
@ -527,7 +534,7 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
h_mine.Owner = player2
h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control")
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
val hMineSource = SourceEntry(h_mine)
val pSource = PlayerSource(player1)
@ -633,6 +640,10 @@ class TurretControlMountTest extends ActorTest {
"control mounting" in {
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
obj.Faction = PlanetSideEmpire.TR
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
assert(obj.Seats(0).occupant.isEmpty)
@ -654,6 +665,10 @@ class TurretControlBlockMountTest extends ActorTest {
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
obj.Faction = PlanetSideEmpire.TR
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
assert(obj.Seats(0).occupant.isEmpty)
val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
@ -701,6 +716,10 @@ class TurretControlDismountTest extends ActorTest {
"control dismounting" in {
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
obj.Faction = PlanetSideEmpire.TR
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
assert(obj.Seats(0).occupant.isEmpty)
@ -733,7 +752,13 @@ class TurretControlBetrayalMountTest extends ActorTest {
MountPoints += 1 -> MountInfo(0, Vector3.Zero)
FactionLocked = false
} //required (defaults to true)
) { GUID = PlanetSideGUID(1) }
) {
GUID = PlanetSideGUID(1)
Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
}
obj.Faction = PlanetSideEmpire.TR
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
val probe = new TestProbe(system)

View file

@ -3,7 +3,9 @@ package objects
import akka.actor.Props
import akka.testkit.TestProbe
import akka.actor.typed.scaladsl.adapter._
import base.ActorTest
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.{Avatar, Certification}
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.definition.ToolDefinition
@ -103,6 +105,10 @@ class FacilityTurretControl2Test extends ActorTest {
val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
val obj = FacilityTurret(GlobalDefinitions.manned_turret)
obj.GUID = PlanetSideGUID(1)
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control")
val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building)
bldg.Amenities = obj
@ -156,6 +162,10 @@ class FacilityTurretControl4Test extends ActorTest {
val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
val obj = FacilityTurret(GlobalDefinitions.vanu_sentry_turret)
obj.GUID = PlanetSideGUID(1)
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control")
val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building)
bldg.Amenities = obj

View file

@ -375,6 +375,8 @@ class GeneratorControlKillsTest extends ActorTest {
guid.register(gen, 2)
guid.register(player1, 3)
guid.register(player2, 4)
zone.blockMap.addTo(player1)
zone.blockMap.addTo(player2)
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile

View file

@ -1,238 +1,214 @@
// Copyright (c) 2021 PSForever
package objects
import net.psforever.objects.geometry._
import net.psforever.objects.geometry.d3._
import net.psforever.types.Vector3
import org.specs2.mutable.Specification
class GeometryTest extends Specification {
"Point3D" should {
"Point" should {
"construct (1)" in {
Point3D(1,2,3.5f)
Point(1,2,3.5f)
ok
}
"construct (2)" in {
Point3D() mustEqual Point3D(0,0,0)
Point() mustEqual Point(0,0,0)
}
"construct (3)" in {
Point3D(Vector3(1,2,3)) mustEqual Point3D(1,2,3)
Point(Vector3(1,2,3)) mustEqual Point(1,2,3)
}
"be its own center point" in {
val obj = Point3D(1,2,3.5f)
val obj = Point(1,2,3.5f)
obj.center mustEqual obj
}
"define its own exterior" in {
val obj = Point3D(1,2,3.5f)
val obj = Point(1,2,3.5f)
obj.pointOnOutside(Vector3(1,0,0)) mustEqual obj
obj.pointOnOutside(Vector3(0,1,0)) mustEqual obj
obj.pointOnOutside(Vector3(0,0,1)) mustEqual obj
}
"convert to Vector3" in {
val obj = Point3D(1,2,3.5f)
val obj = Point(1,2,3.5f)
obj.asVector3 mustEqual Vector3(1,2,3.5f)
}
}
"Ray3D" should {
"Ray" should {
"construct (1)" in {
Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0))
Ray(Point(1,2,3.5f), Vector3(1,0,0))
ok
}
"construct (2)" in {
Ray3D(1,2,3.5f, Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0))
Ray(1,2,3.5f, Vector3(1,0,0)) mustEqual Ray(Point(1,2,3.5f), Vector3(1,0,0))
}
"construct (3)" in {
Ray3D(Vector3(1,2,3.5f), Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0))
Ray(Vector3(1,2,3.5f), Vector3(1,0,0)) mustEqual Ray(Point(1,2,3.5f), Vector3(1,0,0))
}
"have a unit vector as its direction vector" in {
Ray3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError]
Ray(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError]
}
"have its target point as the center point" in {
val obj = Ray3D(1,2,3.5f, Vector3(1,0,0))
obj.center mustEqual Point3D(1,2,3.5f)
}
"define its own exterior" in {
val obj1 = Ray3D(1,2,3.5f, Vector3(1,0,0))
val obj2 = Point3D(1,2,3.5f)
obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2
val obj = Ray(1,2,3.5f, Vector3(1,0,0))
obj.center mustEqual Point(1,2,3.5f)
}
}
"Line3D" should {
"Line" should {
"construct (1)" in {
Line3D(Point3D(1,2,3.5f), Vector3(1,0,0))
Line(Point(1,2,3.5f), Vector3(1,0,0))
ok
}
"construct (2)" in {
Line3D(1,2,3.5f, Vector3(1,0,0))
Line(1,2,3.5f, Vector3(1,0,0))
ok
}
"construct (3)" in {
Line3D(1,2,3.5f, 2,2,3.5f) mustEqual Line3D(1,2,3.5f, Vector3(1,0,0))
Line(1,2,3.5f, 2,2,3.5f) mustEqual Line(1,2,3.5f, Vector3(1,0,0))
}
"have a unit vector as its direction vector" in {
Line3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError]
Line(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError]
}
"have its target point as the center point" in {
val obj = Line3D(1,2,3.5f, Vector3(1,0,0))
obj.center mustEqual Point3D(1,2,3.5f)
}
"define its own exterior" in {
val obj1 = Line3D(1,2,3.5f, Vector3(1,0,0))
val obj2 = Point3D(1,2,3.5f)
obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2
val obj = Line(1,2,3.5f, Vector3(1,0,0))
obj.center mustEqual Point(1,2,3.5f)
}
}
"Segment3D" should {
"Segment" should {
"construct (1)" in {
Segment3D(Point3D(1,2,3), Point3D(3,2,3))
Segment(Point(1,2,3), Point(3,2,3))
ok
}
"construct (2)" in {
Segment3D(1,2,3, 3,2,3) mustEqual Segment3D(Point3D(1,2,3), Point3D(3,2,3))
Segment(1,2,3, 3,2,3) mustEqual Segment(Point(1,2,3), Point(3,2,3))
ok
}
"construct (3)" in {
Segment3D(Point3D(1,2,3), Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3))
Segment(Point(1,2,3), Vector3(1,0,0)) mustEqual Segment(Point(1,2,3), Point(2,2,3))
}
"construct (4)" in {
Segment3D(1,2,3, Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3))
Segment(1,2,3, Vector3(1,0,0)) mustEqual Segment(Point(1,2,3), Point(2,2,3))
}
"does not need to have unit vector as its direction vector" in {
val obj1 = Segment3D(1,2,3, Vector3(5,1,1))
val obj2 = Segment3D(Point3D(1,2,3), Point3D(6,3,4))
val obj1 = Segment(1,2,3, Vector3(5,1,1))
val obj2 = Segment(Point(1,2,3), Point(6,3,4))
obj1 mustEqual obj2
obj1.d mustEqual obj2.d
}
"have a midway point between its two endpoints" in {
Segment3D(Point3D(1,2,3), Point3D(3,4,5)).center mustEqual Point3D(2,3,4)
}
"report the point on the outside as its center point" in {
val obj1 = Segment3D(Point3D(1,2,3), Point3D(3,4,5))
val obj2 = obj1.center
obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2
obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2
Segment(Point(1,2,3), Point(3,4,5)).center mustEqual Point(2,3,4)
}
}
"Sphere3D" should {
"construct (1)" in {
Sphere(Point3D(1,2,3), 3)
Sphere(Point(1,2,3), 3)
ok
}
"construct (2)" in {
Sphere(3) mustEqual Sphere(Point3D(0,0,0), 3)
Sphere(3) mustEqual Sphere(Point(0,0,0), 3)
ok
}
"construct (3)" in {
Sphere(1,2,3, 3) mustEqual Sphere(Point3D(1,2,3), 3)
Sphere(1,2,3, 3) mustEqual Sphere(Point(1,2,3), 3)
}
"construct (4)" in {
Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point3D(1,2,3), 3)
Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point(1,2,3), 3)
}
"the center point is self-evident" in {
Sphere(Point3D(1,2,3), 3).center mustEqual Point3D(1,2,3)
Sphere(Point(1,2,3), 3).center mustEqual Point(1,2,3)
}
"report the point on the outside depending on the requested direction" in {
val obj1 = Sphere(1,2,3, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 4, 2,3) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 5,3) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2,6) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-2, 2,3) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1,-1,3) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2,0) //down
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point( 4, 2,3) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point( 1, 5,3) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point( 1, 2,6) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(-2, 2,3) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point( 1,-1,3) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point( 1, 2,0) //down
}
}
"Cylinder (normal)" should {
"construct (1)" in {
Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
Cylinder(Point(1,2,3), Vector3(0,0,1), 2, 3)
ok
}
"construct (2)" in {
Cylinder(Point3D(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
Cylinder(Point(1,2,3), 2, 3) mustEqual Cylinder(Point(1,2,3), Vector3(0,0,1), 2, 3)
}
"construct (3)" in {
Cylinder(Vector3(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
Cylinder(Vector3(1,2,3), 2, 3) mustEqual Cylinder(Point(1,2,3), Vector3(0,0,1), 2, 3)
}
"construct (4)" in {
Cylinder(Vector3(1,2,3), Vector3(0,0,1), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
Cylinder(Vector3(1,2,3), Vector3(0,0,1), 2, 3) mustEqual Cylinder(Point(1,2,3), Vector3(0,0,1), 2, 3)
}
"report the center point as the center of the cylinder" in {
Cylinder(Point3D(1,2,3), 2, 3).center mustEqual Point3D(1,2,4.5f)
Cylinder(Point(1,2,3), 2, 3).center mustEqual Point(1,2,4.5f)
}
"the point on the outside is different depending on the requested direction" in {
val obj1 = Cylinder(Point3D(1,2,3), 2, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 3, 2, 4.5f) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 4, 4.5f) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2, 6f) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-1, 2, 4.5f) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1, 0, 4.5f) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2, 3f) //down
val obj1 = Cylinder(Point(1,2,3), 2, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point( 3, 2, 4.5f) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point( 1, 4, 4.5f) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point( 1, 2, 6f) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(-1, 2, 4.5f) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point( 1, 0, 4.5f) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point( 1, 2, 3f) //down
}
}
"Cylinder (side tilt)" should {
"not require a specific direction to be relative up" in {
Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3)
Cylinder(Point(1,2,3), Vector3(1,0,0), 2, 3)
ok
}
"require its specific relative up direction to be expressed as a unit vector" in {
Cylinder(Point3D(1,2,3), Vector3(4,0,0), 2, 3) must throwA[AssertionError]
Cylinder(Point(1,2,3), Vector3(4,0,0), 2, 3) must throwA[AssertionError]
}
"report the center point as the center of the cylinder, as if rotated about its base" in {
Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3).center mustEqual Point3D(2.5f, 2, 3)
Cylinder(Point(1,2,3), Vector3(1,0,0), 2, 3).center mustEqual Point(2.5f, 2, 3)
}
"report the point on the outside as different depending on the requested direction and the relative up direction" in {
val obj1 = Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D(4, 2, 3) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D(2.5f, 4, 3) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D(2.5f, 2, 5) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(1, 2, 3) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D(2.5f, 0, 3) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D(2.5f, 2, 1) //down
val obj1 = Cylinder(Point(1,2,3), Vector3(1,0,0), 2, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point(4, 2, 3) //east
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point(2.5f, 4, 3) //north
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point(2.5f, 2, 5) //up
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(1, 2, 3) //west
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point(2.5f, 0, 3) //south
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point(2.5f, 2, 1) //down
val obj2 = Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
val obj2 = Cylinder(Point(1,2,3), Vector3(0,0,1), 2, 3)
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 0))
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 1, 0))
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 1))

View file

@ -6,7 +6,7 @@ import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.environment._
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneMap}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.concurrent.duration._
@ -21,6 +21,9 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
}
new Zone("test-zone", testMap, zoneNumber = 0)
}
testZone.blockMap.addTo(pool1)
testZone.blockMap.addTo(pool2)
testZone.blockMap.addTo(pool3)
"InteractsWithZoneEnvironment" should {
"not interact with any environment when it does not encroach any environment" in {
@ -30,7 +33,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
assert(obj.Position == Vector3.Zero)
obj.zoneInteraction()
obj.zoneInteractions()
testProbe.expectNoMessage(max = 500 milliseconds)
}
@ -41,15 +44,15 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
obj.Position = Vector3(1,1,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.zoneInteraction()
obj.zoneInteractions()
testProbe.expectNoMessage(max = 500 milliseconds)
}
@ -60,17 +63,17 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
obj.Position = Vector3(1,1,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg1 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.Position = Vector3(1,1,1)
obj.zoneInteraction()
obj.zoneInteractions()
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg2 match {
@ -78,7 +81,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
case _ => false
}
)
obj.zoneInteraction()
obj.zoneInteractions()
testProbe.expectNoMessage(max = 500 milliseconds)
}
@ -89,21 +92,21 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
obj.Position = Vector3(7,7,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg1 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.Position = Vector3(12,7,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg2 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2)
case _ => false
}
)
@ -117,17 +120,17 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
obj.Position = Vector3(7,7,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg1 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.Position = Vector3(7,12,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msgs = testProbe.receiveN(2, max = 250 milliseconds)
assert(
msgs.head match {
@ -137,7 +140,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
)
assert(
msgs(1) match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3)
case _ => false
}
)
@ -152,24 +155,22 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Actor = testProbe.ref
obj.Position = Vector3(1,1,-2)
obj.zoneInteraction()
obj.zoneInteractions()
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg1 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.allowZoneEnvironmentInteractions = false
obj.allowInteraction = false
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg2 match {
case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
obj.zoneInteraction()
msg2 match {
case EscapeFromEnvironment(o, b, _) => assert((o eq obj) && (b eq pool1))
case _ => assert( false)
}
obj.zoneInteractions()
testProbe.expectNoMessage(max = 500 milliseconds)
}
@ -179,16 +180,16 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.Zone = testZone
obj.Actor = testProbe.ref
obj.allowZoneEnvironmentInteractions = false
obj.allowInteraction = false
obj.Position = Vector3(1,1,-2)
obj.zoneInteraction()
obj.zoneInteractions()
testProbe.expectNoMessage(max = 500 milliseconds)
obj.allowZoneEnvironmentInteractions = true
obj.allowInteraction = true
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert(
msg1 match {
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
case _ => false
}
)
@ -196,10 +197,11 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
}
object InteractsWithZoneEnvironmentTest {
def testObject(): PlanetSideServerObject with InteractsWithZoneEnvironment = {
def testObject(): PlanetSideServerObject with InteractsWithZone = {
new PlanetSideServerObject
with InteractsWithZoneEnvironment
with InteractsWithZone
with Vitality {
interaction(new InteractWithEnvironment())
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS
def DamageModel = null
def Definition: ObjectDefinition with VitalityDefinition = new ObjectDefinition(objectId = 0) with VitalityDefinition {

View file

@ -2,12 +2,16 @@
package objects
import akka.actor.{Actor, ActorRef, Props}
import akka.testkit.TestProbe
import akka.actor.typed.scaladsl.adapter._
import base.ActorTest
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.Player
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.mount._
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID}
import scala.concurrent.duration.Duration
@ -27,6 +31,10 @@ class MountableControl2Test extends ActorTest {
"let a player mount" in {
val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
val obj = new MountableTest.MountableTestObject
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable")
val msg = Mountable.TryMount(player, 0)
@ -69,9 +77,8 @@ class MountableControl3Test extends ActorTest {
object MountableTest {
class MountableTestObject extends PlanetSideServerObject with Mountable {
seats += 0 -> new Seat(new SeatDefinition())
GUID = PlanetSideGUID(1)
//eh whatever
def Faction = PlanetSideEmpire.TR
GUID = PlanetSideGUID(1) //eh whatever
def Faction = PlanetSideEmpire.TR
def Definition = new ObjectDefinition(1) with MountableDefinition {
MountPoints += 0 -> MountInfo(0)
}

View file

@ -5,7 +5,7 @@ import akka.actor.{ActorRef, Props}
import akka.routing.RandomPool
import akka.testkit.TestProbe
import base.FreedContextActorTest
import net.psforever.actors.zone.BuildingActor
import net.psforever.actors.zone.{BuildingActor, ZoneActor}
import net.psforever.objects.guid.actor.UniqueNumberSystem
import net.psforever.objects.{GlobalDefinitions, Vehicle}
import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
@ -56,6 +56,9 @@ class OrbitalShuttlePadControltest extends FreedContextActorTest {
override def Vehicles = { vehicles.toList }
override def Buildings = { buildingMap.toMap }
override def tasks = { resolver }
import akka.actor.typed.scaladsl.adapter._
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val building = new Building(
name = "test-orbital-building-tr",

View file

@ -786,6 +786,8 @@ class PlayerControlInteractWithWaterTest extends ActorTest {
override def LivePlayers = List(player1)
override def AvatarEvents = avatarProbe.ref
}
zone.blockMap.addTo(player1)
zone.blockMap.addTo(pool)
player1.Zone = zone
player1.Spawn()
@ -799,7 +801,7 @@ class PlayerControlInteractWithWaterTest extends ActorTest {
"cause drowning when player steps too deep in water" in {
assert(player1.Health == 100)
player1.Position = Vector3(5,5,-3) //right in the pool
player1.zoneInteraction() //trigger
player1.zoneInteractions() //trigger
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
assert(
@ -838,6 +840,8 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
override def LivePlayers = List(player1)
override def AvatarEvents = avatarProbe.ref
}
zone.blockMap.addTo(player1)
zone.blockMap.addTo(pool)
player1.Zone = zone
player1.Spawn()
@ -851,7 +855,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
"stop drowning if player steps out of deep water" in {
assert(player1.Health == 100)
player1.Position = Vector3(5,5,-3) //right in the pool
player1.zoneInteraction() //trigger
player1.zoneInteractions() //trigger
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
assert(
@ -865,7 +869,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
)
//player would normally die in 60s
player1.Position = Vector3.Zero //pool's closed
player1.zoneInteraction() //trigger
player1.zoneInteractions() //trigger
val msg_recover = avatarProbe.receiveOne(250 milliseconds)
assert(
msg_recover match {
@ -902,6 +906,8 @@ class PlayerControlInteractWithLavaTest extends ActorTest {
override def AvatarEvents = avatarProbe.ref
override def Activity = TestProbe().ref
}
zone.blockMap.addTo(player1)
zone.blockMap.addTo(pool)
player1.Zone = zone
player1.Spawn()
@ -915,7 +921,7 @@ class PlayerControlInteractWithLavaTest extends ActorTest {
"take continuous damage if player steps into lava" in {
assert(player1.Health == 100) //alive
player1.Position = Vector3(5,5,-3) //right in the pool
player1.zoneInteraction() //trigger
player1.zoneInteractions() //trigger
val msg_burn = avatarProbe.receiveN(3, 1 seconds)
assert(
@ -962,6 +968,8 @@ class PlayerControlInteractWithDeathTest extends ActorTest {
override def AvatarEvents = avatarProbe.ref
override def Activity = TestProbe().ref
}
zone.blockMap.addTo(player1)
zone.blockMap.addTo(pool)
player1.Zone = zone
player1.Spawn()
@ -975,7 +983,7 @@ class PlayerControlInteractWithDeathTest extends ActorTest {
"take continuous damage if player steps into a pool of death" in {
assert(player1.Health == 100) //alive
player1.Position = Vector3(5,5,-3) //right in the pool
player1.zoneInteraction() //trigger
player1.zoneInteractions() //trigger
probe.receiveOne(250 milliseconds) //wait until oplayer1's implants deinitialize
assert(player1.Health == 0) //ded

View file

@ -2,8 +2,10 @@
package objects
import akka.actor.{ActorRef, Props}
import akka.actor.typed.scaladsl.adapter._
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects._
import net.psforever.objects.ce.{DeployableCategory, DeployedItem, TelepadLike}
import net.psforever.objects.guid.NumberPoolHub
@ -33,6 +35,8 @@ class TelepadDeployableNoRouterTest extends ActorTest {
override def AvatarEvents: ActorRef = eventsProbe.ref
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(telepad, number = 1)
@ -96,6 +100,8 @@ class TelepadDeployableNoActivationTest extends ActorTest {
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)
@ -165,6 +171,8 @@ class TelepadDeployableAttemptTest extends ActorTest {
override def LocalEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)
@ -226,6 +234,8 @@ class TelepadDeployableResponseFromRouterTest extends ActorTest {
override def VehicleEvents: ActorRef = eventsProbe.ref
override def Deployables: ActorRef = deployables
override def Vehicles: List[Vehicle] = List(router)
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
guid.register(telepad, number = 1)
guid.register(router, number = 2)

View file

@ -329,6 +329,7 @@ class VehicleControlMountingBlockedExosuitTest extends ActorTest {
override def LocalEvents: ActorRef = catchall
override def VehicleEvents: ActorRef = catchall
override def Activity: ActorRef = catchall
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val vehicle = Vehicle(GlobalDefinitions.apc_tr)
vehicle.Faction = PlanetSideEmpire.TR
@ -392,6 +393,10 @@ class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest {
vehicle.Faction = PlanetSideEmpire.TR
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
override def SetupNumberPools(): Unit = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val player1 = Player(VehicleTest.avatar1)
player1.GUID = PlanetSideGUID(1)
@ -418,6 +423,10 @@ class VehicleControlMountingDriverSeatTest extends ActorTest {
vehicle.Faction = PlanetSideEmpire.TR
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
override def SetupNumberPools(): Unit = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val player1 = Player(VehicleTest.avatar1)
player1.GUID = PlanetSideGUID(1)
@ -439,6 +448,11 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest {
vehicle.Faction = PlanetSideEmpire.TR
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
override def SetupNumberPools(): Unit = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val player1 = Player(VehicleTest.avatar1)
player1.GUID = PlanetSideGUID(1)
val player2 = Player(VehicleTest.avatar1)
@ -470,6 +484,11 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest {
vehicle.Faction = PlanetSideEmpire.TR
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
override def SetupNumberPools(): Unit = {}
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val player1 = Player(VehicleTest.avatar1)
player1.GUID = PlanetSideGUID(1)
val player2 = Player(VehicleTest.avatar1)
@ -640,6 +659,8 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest {
override def LivePlayers = List(player1)
override def Vehicles = List(vehicle)
}
zone.blockMap.addTo(vehicle)
zone.blockMap.addTo(pool)
guid.register(player1, 1)
guid.register(vehicle, 2)
@ -655,12 +676,12 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest {
"VehicleControl" should {
"causes disability when the vehicle drives too deep in water (check driver messaging)" in {
vehicle.Position = Vector3(5,5,-3) //right in the pool
vehicle.zoneInteraction() //trigger
vehicle.zoneInteractions() //trigger
val msg_drown = playerProbe.receiveOne(250 milliseconds)
assert(
msg_drown match {
case InteractWithEnvironment(
case InteractingWithEnvironment(
p1,
p2,
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f))
@ -693,7 +714,11 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
override def Vehicles = List(vehicle)
override def AvatarEvents = avatarProbe.ref
override def VehicleEvents = vehicleProbe.ref
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
zone.blockMap.addTo(vehicle)
zone.blockMap.addTo(pool)
guid.register(player1, 1)
guid.register(vehicle, 2)
@ -711,7 +736,7 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
"VehicleControl" should {
"causes disability when the vehicle drives too deep in water" in {
vehicle.Position = Vector3(5,5,-3) //right in the pool
vehicle.zoneInteraction() //trigger
vehicle.zoneInteractions() //trigger
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
assert(
@ -728,16 +753,14 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
)
//player will die in 60s
//vehicle will disable in 5s; driver will be kicked
val msg_kick = vehicleProbe.receiveOne(6 seconds)
assert(
msg_kick match {
case VehicleServiceMessage(
"test-zone",
VehicleAction.KickPassenger(PlanetSideGUID(1), 4, _, PlanetSideGUID(2))
) => true
case _ => false
}
)
val msg_kick = vehicleProbe.receiveOne(10 seconds)
msg_kick match {
case VehicleServiceMessage(
"test-zone",
VehicleAction.KickPassenger(PlanetSideGUID(1), 4, _, PlanetSideGUID(2))
) => assert(true)
case _ => assert(false)
}
//player will die, but detailing players death messages is not necessary for this test
}
}
@ -762,6 +785,8 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
override def LivePlayers = List(player1)
override def Vehicles = List(vehicle)
}
zone.blockMap.addTo(vehicle)
zone.blockMap.addTo(pool)
guid.register(player1, 1)
guid.register(vehicle, 2)
@ -777,11 +802,11 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
"VehicleControl" should {
"stop becoming disabled if the vehicle drives out of the water" in {
vehicle.Position = Vector3(5,5,-3) //right in the pool
vehicle.zoneInteraction() //trigger
vehicle.zoneInteractions() //trigger
val msg_drown = playerProbe.receiveOne(250 milliseconds)
assert(
msg_drown match {
case InteractWithEnvironment(
case InteractingWithEnvironment(
p1,
p2,
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f))
@ -791,7 +816,7 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
)
vehicle.Position = Vector3.Zero //that's enough of that
vehicle.zoneInteraction()
vehicle.zoneInteractions()
val msg_recover = playerProbe.receiveOne(250 milliseconds)
assert(
msg_recover match {
@ -830,6 +855,8 @@ class VehicleControlInteractWithLavaTest extends ActorTest {
override def VehicleEvents = vehicleProbe.ref
override def Activity = TestProbe().ref
}
zone.blockMap.addTo(vehicle)
zone.blockMap.addTo(pool)
guid.register(player1, 1)
guid.register(vehicle, 2)
@ -849,7 +876,7 @@ class VehicleControlInteractWithLavaTest extends ActorTest {
assert(vehicle.Health > 0) //alive
assert(player1.Health == 100) //alive
vehicle.Position = Vector3(5,5,-3) //right in the pool
vehicle.zoneInteraction() //trigger
vehicle.zoneInteractions() //trigger
val msg_burn = vehicleProbe.receiveN(3,1 seconds)
msg_burn.foreach { msg =>
@ -888,6 +915,8 @@ class VehicleControlInteractWithDeathTest extends ActorTest {
override def AvatarEvents = TestProbe().ref
override def VehicleEvents = TestProbe().ref
}
zone.blockMap.addTo(vehicle)
zone.blockMap.addTo(pool)
guid.register(player1, 1)
guid.register(vehicle, 2)
@ -907,7 +936,7 @@ class VehicleControlInteractWithDeathTest extends ActorTest {
assert(vehicle.Health > 0) //alive
assert(player1.Health == 100) //alive
vehicle.Position = Vector3(5,5,-3) //right in the pool
vehicle.zoneInteraction() //trigger
vehicle.zoneInteractions() //trigger
probe.receiveOne(2 seconds) //wait until player1's implants deinitialize
assert(vehicle.Health == 0) //ded

View file

@ -2,7 +2,9 @@
package objects.terminal
import akka.actor.{ActorSystem, Props}
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.{Default, GlobalDefinitions, Player}
import net.psforever.objects.guid.NumberPoolHub
@ -156,11 +158,14 @@ class ImplantTerminalMechControl5Test extends ActorTest {
object ImplantTerminalMechTest {
def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, ImplantTerminalMech) = {
import akka.actor.typed.scaladsl.adapter._
val guid = new NumberPoolHub(new MaxNumberSource(10))
val map = new ZoneMap("test")
val zone = new Zone("test", map, 0) {
override def SetupNumberPools() = {}
GUID(guid)
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
}
val building = new Building(
"Building",