From 3966b0264dccfe824354b313ff8bf1692a95baef Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Fri, 11 Jun 2021 23:02:48 -0400 Subject: [PATCH] The Blockmap (#852) * separating geometry classes * 2d geometry; retirement of the *3D suffix * makings of an early block map datastructure * entities in a zone - players, corpses, vehicles, deployables, ground clutter, and buildings - divided between sectors of the zone upon creation, management, or mounting; superfluous messages to keep track of blockmap state, for now * trait for entities to be added to the blockmap; internal entity data keeps track of current blockmap sector information; calls to add/remove/update functions changed * modified pieces of environment into an entities that can be added to a block map and have a countable bounding region; fixes for vehicle control seat occcupant collection; fix for squad individual callback references (original issue still remains?) * introduced the block map into various existijng game calculationa where target selection can be reduced by its probing * he_mines and jammer_mines now trigger if a valid target is detected at the initial point of deploy; they also trigger later, after a valid target has moved into the arming range of the mine * conversion of interactions with zone into a queued, periodic set of tasks * explosive deployable control -> mine deployable control * tests repaired and all tests working * mostly comments and documentation * amenities are now represented on the blockmap --- .../actors/session/SessionActor.scala | 38 +- .../net/psforever/actors/zone/ZoneActor.scala | 18 + .../psforever/objects/BoomerDeployable.scala | 5 +- .../objects/ExplosiveDeployable.scala | 144 ++++- .../psforever/objects/GlobalDefinitions.scala | 34 ++ .../scala/net/psforever/objects/Player.scala | 13 +- .../net/psforever/objects/SpecialEmp.scala | 10 +- .../scala/net/psforever/objects/Vehicle.scala | 11 +- .../objects/avatar/PlayerControl.scala | 7 +- .../net/psforever/objects/ce/Deployable.scala | 2 + .../objects/ce/DeployableBehavior.scala | 3 + .../objects/ce/InteractWithMines.scala | 53 ++ .../objects/definition/ObjectDefinition.scala | 9 +- .../objects/equipment/Equipment.scala | 6 +- .../objects/geometry/AxisAlignment.scala | 90 ++++ .../psforever/objects/geometry/Geometry.scala | 49 +- .../objects/geometry/GeometryForm.scala | 44 +- .../objects/geometry/PrimitiveGeometry.scala | 102 ++++ .../objects/geometry/PrimitiveShape.scala | 503 ------------------ .../objects/geometry/d2/Geometry2D.scala | 18 + .../psforever/objects/geometry/d2/Point.scala | 76 +++ .../objects/geometry/d2/Rectangle.scala | 46 ++ .../objects/geometry/d3/Cuboid.scala | 78 +++ .../objects/geometry/d3/Cylinder.scala | 100 ++++ .../objects/geometry/d3/Geometry3D.scala | 28 + .../psforever/objects/geometry/d3/Line.scala | 60 +++ .../psforever/objects/geometry/d3/Point.scala | 43 ++ .../psforever/objects/geometry/d3/Ray.scala | 44 ++ .../objects/geometry/d3/Segment.scala | 71 +++ .../objects/geometry/d3/Sphere.scala | 62 +++ .../environment/EnvironmentCollision.scala | 15 + .../environment/InteractWithEnvironment.scala | 199 +++++-- .../InteractingWithEnvironment.scala | 49 ++ .../InteractsWithZoneEnvironment.scala | 185 ------- .../environment/PieceOfEnvironment.scala | 18 +- .../RespondsToZoneEnvironment.scala | 11 +- .../mount/MountableBehavior.scala | 3 + .../serverobject/structures/Amenity.scala | 4 +- .../serverobject/structures/Building.scala | 5 +- .../objects/vehicles/CargoBehavior.scala | 11 +- .../objects/vehicles/VehicleControl.scala | 15 +- .../objects/vital/etc/TrippedMineReason.scala | 42 ++ .../objects/zones/InteractsWithZone.scala | 76 +++ .../net/psforever/objects/zones/MapInfo.scala | 2 +- .../zones/SphereOfInfluenceActor.scala | 6 +- .../net/psforever/objects/zones/Zone.scala | 57 +- .../objects/zones/ZoneDeployableActor.scala | 5 +- .../objects/zones/ZoneGroundActor.scala | 34 +- .../objects/zones/ZonePopulationActor.scala | 13 + .../objects/zones/ZoneVehicleActor.scala | 5 + .../objects/zones/blockmap/BlockMap.scala | 397 ++++++++++++++ .../zones/blockmap/BlockMapEntity.scala | 92 ++++ .../objects/zones/blockmap/Sector.scala | 282 ++++++++++ .../services/teamwork/SquadService.scala | 26 +- .../objects/DeployableBehaviorTest.scala | 12 + src/test/scala/objects/DeployableTest.scala | 33 +- .../scala/objects/FacilityTurretTest.scala | 10 + src/test/scala/objects/GeneratorTest.scala | 2 + src/test/scala/objects/GeometryTest.scala | 152 +++--- .../InteractsWithZoneEnvironmentTest.scala | 68 +-- src/test/scala/objects/MountableTest.scala | 13 +- .../scala/objects/OrbitalShuttlePadTest.scala | 5 +- .../scala/objects/PlayerControlTest.scala | 18 +- .../scala/objects/TelepadRouterTest.scala | 10 + .../scala/objects/VehicleControlTest.scala | 65 ++- .../terminal/ImplantTerminalMechTest.scala | 5 + 66 files changed, 2701 insertions(+), 1011 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/ce/InteractWithMines.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/AxisAlignment.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/PrimitiveGeometry.scala delete mode 100644 src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d2/Geometry2D.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d2/Point.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d2/Rectangle.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Cuboid.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Geometry3D.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Line.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Point.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Ray.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Segment.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/d3/Sphere.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala delete mode 100644 src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala create mode 100644 src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala create mode 100644 src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala create mode 100644 src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala create mode 100644 src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala create mode 100644 src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index a94834e0..c69797e9 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 55762314..149fb0e4 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 b4fc98f3..e40a1993 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 00322346..30bc205c 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 7db52cd5..7feb758f 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 e86d576f..46ac7d6c 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 17769771..1f2d1b79 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 633cb528..f21ecebe 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 fd251119..85f4af65 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 504450e0..d9025f8c 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 c7c4d151..4d6e512d 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 00000000..3d3d8394 --- /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 0df09165..b4cc26d3 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 5168f636..d1d2b9fc 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 00000000..a1fc58ed --- /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 ce796b75..186e4555 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 8614163e..5586c3de 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 00000000..e9a0d74e --- /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 7d7c72f5..00000000 --- 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 00000000..fb53e891 --- /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 00000000..8f0e0b85 --- /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 00000000..41ac276a --- /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 00000000..416d78d5 --- /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 00000000..c7c909a4 --- /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 00000000..f474c614 --- /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 00000000..26b6425b --- /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 00000000..b2176f1f --- /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 00000000..03be91d9 --- /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 00000000..ce80bcaf --- /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 00000000..59c4b1fd --- /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 7187fcad..bd60a69a 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 5a757875..7b1752f6 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 00000000..d514624c --- /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 7d59d962..00000000 --- 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 aa9d3274..4f8ffc12 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 0e1feb03..f243c317 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 9d30a999..7617efa7 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 cd03d286..29dc28fa 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 107a2053..1e59f8a1 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 8bff015a..900f5d69 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 7d255c7f..fff3c7fe 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 00000000..2da819e2 --- /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 00000000..c5341478 --- /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 f06d3452..345f5697 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 461a126a..0510d5c0 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 4eb851d4..669abbc4 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 e946d0fc..3921506c 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 0ebf2a28..cf41f29d 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 083828cb..2b4c3256 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 28071573..c07e827c 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 00000000..5aac419d --- /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 00000000..325a1fb5 --- /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 00000000..2cbfeb42 --- /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 c9254789..bcaa798d 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 1cce70a7..38e4703b 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 e85471f8..7a9713f7 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 daf7dbe5..359bafa7 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 71aa08ec..feb62dc0 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 05640453..764ed70b 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 d6d1b8f8..de43ae73 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 2dc7a45e..b44b2cf9 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 cce0ba30..877bef4e 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 a6272631..42aa9f15 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 683f5e37..b95c04af 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 8a6f6353..8ce6a880 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 eb046576..1f971235 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",