diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala
index a94834e09..c69797e9f 100644
--- a/src/main/scala/net/psforever/actors/session/SessionActor.scala
+++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala
@@ -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()
diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
index 55762314d..149fb0e48 100644
--- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
+++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
@@ -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)
diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
index b4fc98f30..e40a19937 100644
--- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala
+++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
index 003223464..30bc205c2 100644
--- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
+++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
@@ -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)
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index 7db52cd54..7feb758fa 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -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"
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index e86d576f6..46ac7d6ca 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala
index 177697716..1f2d1b79c 100644
--- a/src/main/scala/net/psforever/objects/SpecialEmp.scala
+++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala
@@ -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 }
}
}
diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala
index 633cb5283..f21ecebea 100644
--- a/src/main/scala/net/psforever/objects/Vehicle.scala
+++ b/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
index fd251119e..85f4af653 100644
--- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
+++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/ce/Deployable.scala b/src/main/scala/net/psforever/objects/ce/Deployable.scala
index 504450e07..d9025f8c9 100644
--- a/src/main/scala/net/psforever/objects/ce/Deployable.scala
+++ b/src/main/scala/net/psforever/objects/ce/Deployable.scala
@@ -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 {
diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
index c7c4d151e..4d6e512d8 100644
--- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala
new file mode 100644
index 000000000..3d3d83943
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala
@@ -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()
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala
index 0df091659..b4cc26d30 100644
--- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala
+++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala
@@ -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
}
diff --git a/src/main/scala/net/psforever/objects/equipment/Equipment.scala b/src/main/scala/net/psforever/objects/equipment/Equipment.scala
index 5168f6368..d1d2b9fce 100644
--- a/src/main/scala/net/psforever/objects/equipment/Equipment.scala
+++ b/src/main/scala/net/psforever/objects/equipment/Equipment.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/geometry/AxisAlignment.scala b/src/main/scala/net/psforever/objects/geometry/AxisAlignment.scala
new file mode 100644
index 000000000..a1fc58eda
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/AxisAlignment.scala
@@ -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)
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala
index ce796b756..186e4555c 100644
--- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala
+++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala
@@ -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
}
}
}
diff --git a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala
index 8614163e7..5586c3de6 100644
--- a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala
+++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala
@@ -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)
diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveGeometry.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveGeometry.scala
new file mode 100644
index 000000000..e9a0d74ec
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveGeometry.scala
@@ -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
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala
deleted file mode 100644
index 7d7c72f5d..000000000
--- a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala
+++ /dev/null
@@ -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))
- }
-}
diff --git a/src/main/scala/net/psforever/objects/geometry/d2/Geometry2D.scala b/src/main/scala/net/psforever/objects/geometry/d2/Geometry2D.scala
new file mode 100644
index 000000000..fb53e8915
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d2/Geometry2D.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/geometry/d2/Point.scala b/src/main/scala/net/psforever/objects/geometry/d2/Point.scala
new file mode 100644
index 000000000..8f0e0b851
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d2/Point.scala
@@ -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)
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d2/Rectangle.scala b/src/main/scala/net/psforever/objects/geometry/d2/Rectangle.scala
new file mode 100644
index 000000000..41ac276af
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d2/Rectangle.scala
@@ -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)
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Cuboid.scala b/src/main/scala/net/psforever/objects/geometry/d3/Cuboid.scala
new file mode 100644
index 000000000..416d78d58
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Cuboid.scala
@@ -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))
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala b/src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala
new file mode 100644
index 000000000..c7c909a4e
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala
@@ -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)
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Geometry3D.scala b/src/main/scala/net/psforever/objects/geometry/d3/Geometry3D.scala
new file mode 100644
index 000000000..f474c6149
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Geometry3D.scala
@@ -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
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Line.scala b/src/main/scala/net/psforever/objects/geometry/d3/Line.scala
new file mode 100644
index 000000000..26b6425b1
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Line.scala
@@ -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)))
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Point.scala b/src/main/scala/net/psforever/objects/geometry/d3/Point.scala
new file mode 100644
index 000000000..b2176f1f8
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Point.scala
@@ -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)
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Ray.scala b/src/main/scala/net/psforever/objects/geometry/d3/Ray.scala
new file mode 100644
index 000000000..03be91d98
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Ray.scala
@@ -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)
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Segment.scala b/src/main/scala/net/psforever/objects/geometry/d3/Segment.scala
new file mode 100644
index 000000000..ce80bcafc
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Segment.scala
@@ -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))
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/geometry/d3/Sphere.scala b/src/main/scala/net/psforever/objects/geometry/d3/Sphere.scala
new file mode 100644
index 000000000..59c4b1fd5
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/geometry/d3/Sphere.scala
@@ -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)
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala
index 7187fcad0..bd60a69ad 100644
--- a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala
@@ -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
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala
index 5a7578750..7b1752f6c 100644
--- a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala
@@ -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
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala
new file mode 100644
index 000000000..d514624c8
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala
@@ -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()
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala
deleted file mode 100644
index 7d59d9624..000000000
--- a/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala
index aa9d3274a..4f8ffc121 100644
--- a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala
@@ -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 {
diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala
index 0e1feb033..f243c317f 100644
--- a/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala
@@ -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) =>
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
index 9d30a9990..7617efa73 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
@@ -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))
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala
index cd03d2860..29dc28fab 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala
@@ -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 */
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
index 107a20530..1e59f8a1b 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
index 8bff015a4..900f5d691 100644
--- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
@@ -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 =
diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala
index 7d255c7f2..fff3c7fe6 100644
--- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala
@@ -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) }
)
}
diff --git a/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala
new file mode 100644
index 000000000..2da819e27
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala
@@ -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
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala
new file mode 100644
index 000000000..c5341478d
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala
@@ -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
+}
diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala
index f06d3452d..345f5697a 100644
--- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala
+++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala
index 461a126a2..0510d5c07 100644
--- a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala
+++ b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala
@@ -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())
diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala
index 4eb851d49..669abbc41 100644
--- a/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -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
diff --git a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala
index e946d0fc9..3921506cc 100644
--- a/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala
+++ b/src/main/scala/net/psforever/objects/zones/ZoneDeployableActor.scala
@@ -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(_) => ;
diff --git a/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala
index 0ebf2a282..cf41f29d7 100644
--- a/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala
+++ b/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala
@@ -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 => ;
}
diff --git a/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala
index 083828cbd..2b4c32564 100644
--- a/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala
+++ b/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala
@@ -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 _ => ;
diff --git a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala
index 280715732..c07e827c7 100644
--- a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala
+++ b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala
@@ -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")
diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala
new file mode 100644
index 000000000..5aac419de
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala
@@ -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.
+ *
+ * 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)
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala
new file mode 100644
index 000000000..325a1fb53
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala
@@ -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
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala
new file mode 100644
index 000000000..2cbfeb42a
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala
@@ -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
+ )
+ }
+}
diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala
index c9254789b..bcaa798d6 100644
--- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala
+++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala
@@ -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 {
diff --git a/src/test/scala/objects/DeployableBehaviorTest.scala b/src/test/scala/objects/DeployableBehaviorTest.scala
index 1cce70a72..38e4703b5 100644
--- a/src/test/scala/objects/DeployableBehaviorTest.scala
+++ b/src/test/scala/objects/DeployableBehaviorTest.scala
@@ -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)
diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala
index e85471f8a..7a9713f7d 100644
--- a/src/test/scala/objects/DeployableTest.scala
+++ b/src/test/scala/objects/DeployableTest.scala
@@ -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)
diff --git a/src/test/scala/objects/FacilityTurretTest.scala b/src/test/scala/objects/FacilityTurretTest.scala
index daf7dbe5a..359bafa7a 100644
--- a/src/test/scala/objects/FacilityTurretTest.scala
+++ b/src/test/scala/objects/FacilityTurretTest.scala
@@ -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
diff --git a/src/test/scala/objects/GeneratorTest.scala b/src/test/scala/objects/GeneratorTest.scala
index 71aa08ec3..feb62dc09 100644
--- a/src/test/scala/objects/GeneratorTest.scala
+++ b/src/test/scala/objects/GeneratorTest.scala
@@ -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
diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala
index 056404534..764ed70be 100644
--- a/src/test/scala/objects/GeometryTest.scala
+++ b/src/test/scala/objects/GeometryTest.scala
@@ -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))
diff --git a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala
index d6d1b8f8c..de43ae73b 100644
--- a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala
+++ b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala
@@ -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 {
diff --git a/src/test/scala/objects/MountableTest.scala b/src/test/scala/objects/MountableTest.scala
index 2dc7a45ee..b44b2cf9c 100644
--- a/src/test/scala/objects/MountableTest.scala
+++ b/src/test/scala/objects/MountableTest.scala
@@ -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)
}
diff --git a/src/test/scala/objects/OrbitalShuttlePadTest.scala b/src/test/scala/objects/OrbitalShuttlePadTest.scala
index cce0ba303..877bef4e6 100644
--- a/src/test/scala/objects/OrbitalShuttlePadTest.scala
+++ b/src/test/scala/objects/OrbitalShuttlePadTest.scala
@@ -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",
diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala
index a62726315..42aa9f153 100644
--- a/src/test/scala/objects/PlayerControlTest.scala
+++ b/src/test/scala/objects/PlayerControlTest.scala
@@ -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
diff --git a/src/test/scala/objects/TelepadRouterTest.scala b/src/test/scala/objects/TelepadRouterTest.scala
index 683f5e37c..b95c04af2 100644
--- a/src/test/scala/objects/TelepadRouterTest.scala
+++ b/src/test/scala/objects/TelepadRouterTest.scala
@@ -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)
diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala
index 8a6f6353f..8ce6a880f 100644
--- a/src/test/scala/objects/VehicleControlTest.scala
+++ b/src/test/scala/objects/VehicleControlTest.scala
@@ -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
diff --git a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala
index eb0465764..1f9712356 100644
--- a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala
+++ b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala
@@ -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",