diff --git a/common/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala b/common/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala
new file mode 100644
index 00000000..4a315c25
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala
@@ -0,0 +1,162 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.zones
+
+import net.psforever.types.{PlanetSideEmpire, Vector3}
+
+import scala.concurrent.duration._
+
+/**
+ * Information necessary to determine if a hotspot should be displayed.
+ * Hotspots are used on the zone maps to indicate activity.
+ * Each of the factions will view different hotspot configurations
+ * but one faction may encounter hotspots in the same places as another faction
+ * with information referring to the same encounter.
+ * @param DisplayLocation the coordinates where the hotspot will be depicted on some zone's map
+ */
+class HotSpotInfo(val DisplayLocation : Vector3) {
+ private val activity : Map[PlanetSideEmpire.Value, ActivityReport] = Map(
+ PlanetSideEmpire.TR -> new ActivityReport(),
+ PlanetSideEmpire.NC -> new ActivityReport(),
+ PlanetSideEmpire.VS -> new ActivityReport()
+ )
+
+ def Activity : Map[PlanetSideEmpire.Value, ActivityReport] = activity
+
+ def ActivityFor(faction : PlanetSideEmpire.Value) : Option[ActivityReport] = {
+ activity.get(faction)
+ }
+
+ /**
+ * Which factions claim a current level of activity that they might see this hotspot?
+ * @return the active factions
+ */
+ def ActivityBy() : Set[PlanetSideEmpire.Value] = {
+ for {
+ faction <- PlanetSideEmpire.values
+ if ActivityBy(faction)
+ } yield faction
+ }
+
+ /**
+ * Does a specific faction claim a current level of activity that they might see this hotspot?
+ * @param faction the faction
+ * @return `true`, if the heat level is non-zero;
+ * `false`, otherwise
+ */
+ def ActivityBy(faction : PlanetSideEmpire.Value) : Boolean = {
+ activity.get(faction) match {
+ case Some(report) =>
+ report.Heat > 0
+ case None =>
+ false
+ }
+ }
+}
+
+/**
+ * Information about interactions in respect to a given denomination in the game world.
+ * In terms of hotspots, the "denomination" are the factions.
+ * While a given report of activity will only be valid from the time or arrival for a given amount of time,
+ * subsequent activity reporting before this duration concludes will cause the lifespan to artificially increase.
+ */
+class ActivityReport {
+ /** heat increases each time the hotspot is considered active and receives more activity */
+ private var heat : Int = 0
+ /** the time of the last activity report */
+ private var lastReport : Option[Long] = None
+ /** the length of time from the last reporting that this (ongoing) activity will stay relevant */
+ private var duration : FiniteDuration = 0 seconds
+
+ /**
+ * The increasing heat does nothing, presently, but acts as a flag for activity.
+ * @return the heat
+ */
+ def Heat : Int = heat
+
+ /**
+ * As a `Long` value, if there was no previous report, the value will be considered `0L`.
+ * @return the time of the last activity report
+ */
+ def LastReport : Long = lastReport match { case Some(t) => t; case _ => 0L }
+
+ /**
+ * The length of time that this (ongoing) activity is relevant.
+ * @return the time
+ */
+ def Duration : FiniteDuration = duration
+
+ /**
+ * Set the length of time that this (ongoing) activity is relevant.
+ * @param time the time, as a `Duration`
+ * @return the time
+ */
+ def Duration_=(time : FiniteDuration) : FiniteDuration = {
+ Duration_=(time.toNanos)
+ }
+
+ /**
+ * Set the length of time that this (ongoing) activity is relevant.
+ * The duration length can only increase.
+ * @param time the time, as a `Long` value
+ * @return the time
+ */
+ def Duration_=(time : Long) : FiniteDuration = {
+ if(time > duration.toNanos) {
+ duration = FiniteDuration(time, "nanoseconds")
+ Renew
+ }
+ Duration
+ }
+
+ /**
+ * Submit new activity, increasing the lifespan of the current report's existence.
+ * @see `Renew`
+ * @return the current report
+ */
+ def Report() : ActivityReport = {
+ RaiseHeat(1)
+ Renew
+ this
+ }
+
+ /**
+ * Submit new activity, increasing the lifespan of the current report's existence.
+ * @see `Renew`
+ * @return the current report
+ */
+ def Report(pow : Int) : ActivityReport = {
+ RaiseHeat(pow)
+ Renew
+ this
+ }
+
+ private def RaiseHeat(addHeat : Int) : Int = {
+ if(addHeat < (Integer.MAX_VALUE - heat)) {
+ heat += addHeat
+ }
+ else {
+ heat = Integer.MAX_VALUE
+ }
+ heat
+ }
+
+ /**
+ * Reset the time of the last report to the present.
+ * @return the current time
+ */
+ def Renew : Long = {
+ val t = System.nanoTime
+ lastReport = Some(t)
+ t
+ }
+
+ /**
+ * Act as if no activity was ever valid for this report.
+ * Set heat to zero to flag no activity and set duration to "0 seconds" to eliminate its lifespan.
+ */
+ def Clear() : Unit = {
+ heat = 0
+ lastReport = None
+ duration = FiniteDuration(0, "nanoseconds")
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/MapScale.scala b/common/src/main/scala/net/psforever/objects/zones/MapScale.scala
new file mode 100644
index 00000000..8a8a7c0e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/MapScale.scala
@@ -0,0 +1,19 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.zones
+
+/**
+ * An object representing the dimensions of the zone map as its maximum coordinates.
+ * @see `InventoryTile`
+ * @param width the longitudinal span of the map
+ * @param height the latitudinal span of the map
+ */
+final case class MapScale(width : Float, height : Float)
+
+object MapScale {
+ final val Dim512 = MapScale(512, 512) //map49 (unused)
+ final val Dim1024 = MapScale(1024, 1024) //homebo, tzsh*, tzco*
+ final val Dim2048 = MapScale(2048, 2048) //ugd3 .. ugd5; map44 .. map46, map80, map95 (unused)
+ final val Dim2560 = MapScale(2560, 2560) //ugd1, ugd2, ugd6
+ final val Dim4096 = MapScale(4096, 4096) //tzdr*
+ final val Dim8192 = MapScale(8192, 8192) //common size
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
index b99c4376..0c12ecd5 100644
--- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props}
import akka.routing.RandomPool
-import net.psforever.objects.ballistics.Projectile
+import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects._
import net.psforever.objects.ce.Deployable
import net.psforever.objects.entity.IdentifiableEntity
@@ -19,12 +19,13 @@ import net.psforever.objects.serverobject.structures.{Amenity, Building, WarpGat
import net.psforever.objects.serverobject.terminals.ProximityUnit
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.packet.game.PlanetSideGUID
-import net.psforever.types.Vector3
+import net.psforever.types.{PlanetSideEmpire, Vector3}
import services.Service
import scala.collection.concurrent.TrieMap
import scala.collection.mutable.ListBuffer
import scala.collection.immutable.{Map => PairMap}
+import scala.concurrent.duration._
/**
* A server object representing the one-landmass planets as well as the individual subterranean caverns.
@@ -76,6 +77,14 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
/** key - spawn zone id, value - buildings belonging to spawn zone */
private var spawnGroups : Map[Building, List[SpawnPoint]] = PairMap[Building, List[SpawnPoint]]()
/** */
+ private var projector : ActorRef = ActorRef.noSender
+ /** */
+ private var hotspots : ListBuffer[HotSpotInfo] = ListBuffer[HotSpotInfo]()
+ /** calculate a approximated coordinate from a raw input coordinate */
+ private var hotspotCoordinateFunc : Vector3=>Vector3 = Zone.HotSpot.Rules.OneToOne
+ /** calculate a duration from a given interaction's participants */
+ private var hotspotTimeFunc : (SourceEntry, SourceEntry)=>FiniteDuration = Zone.HotSpot.Rules.NoTime
+ /** */
private var vehicleEvents : ActorRef = ActorRef.noSender
/**
@@ -104,6 +113,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
deployables = context.actorOf(Props(classOf[ZoneDeployableActor], this, constructions), s"$Id-deployables")
transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles")
population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players")
+ projector = context.actorOf(Props(classOf[ZoneHotSpotProjector], this), s"$Id-hotpots")
BuildLocalObjects(context, guid)
BuildSupportObjects()
@@ -414,6 +424,45 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
entry
}
+ def Activity : ActorRef = projector
+
+ def HotSpots : List[HotSpotInfo] = hotspots toList
+
+ def HotSpots_=(spots : Seq[HotSpotInfo]) : List[HotSpotInfo] = {
+ hotspots.clear
+ hotspots ++= spots
+ HotSpots
+ }
+
+ def TryHotSpot(displayLoc : Vector3) : HotSpotInfo = {
+ hotspots.find(spot => spot.DisplayLocation == displayLoc) match {
+ case Some(spot) =>
+ //hotspot already exists
+ spot
+ case None =>
+ //insert new hotspot
+ val spot = new HotSpotInfo(displayLoc)
+ hotspots += spot
+ spot
+ }
+ }
+
+ def HotSpotCoordinateFunction : Vector3=>Vector3 = hotspotCoordinateFunc
+
+ def HotSpotCoordinateFunction_=(func : Vector3=>Vector3) : Vector3=>Vector3 = {
+ hotspotCoordinateFunc = func
+ Activity ! ZoneHotSpotProjector.UpdateMappingFunction()
+ HotSpotCoordinateFunction
+ }
+
+ def HotSpotTimeFunction : (SourceEntry, SourceEntry)=>FiniteDuration = hotspotTimeFunc
+
+ def HotSpotTimeFunction_=(func : (SourceEntry, SourceEntry)=>FiniteDuration) : (SourceEntry, SourceEntry)=>FiniteDuration = {
+ hotspotTimeFunc = func
+ Activity ! ZoneHotSpotProjector.UpdateDurationFunction()
+ HotSpotTimeFunction
+ }
+
/**
* Provide bulk correspondence on all map entities that can be composed into packet messages and reported to a client.
* These messages are sent in this fashion at the time of joining the server:
@@ -585,6 +634,38 @@ object Zone {
final case class CanNotDespawn(zone : Zone, vehicle : Vehicle, reason : String)
}
+ object HotSpot {
+ final case class Activity(defender : SourceEntry, attacker : SourceEntry, location : Vector3)
+
+ final case class Cleanup()
+
+ final case class ClearAll()
+
+ final case class Update(faction : PlanetSideEmpire.Value, zone_num : Int, priority : Int, info : List[HotSpotInfo])
+
+ final case class UpdateNow()
+
+ object Rules {
+ /**
+ * Produce hotspot coordinates based on map coordinates.
+ * Return the same coordinate as output that was input.
+ * The default function.
+ * @param pos the absolute position of the activity reported
+ * @return the position for a hotspot
+ */
+ def OneToOne(pos : Vector3) : Vector3 = pos
+
+ /**
+ * Determine a duration for which the hotspot will be displayed on the zone map.
+ * The default function.
+ * @param defender the defending party
+ * @param attacker the attacking party
+ * @return the duration
+ */
+ def NoTime(defender : SourceEntry, attacker : SourceEntry) : FiniteDuration = 0 seconds
+ }
+ }
+
/**
* Message to report the packet messages that initialize the client.
* @param zone a `Zone` to have its buildings and continental parameters turned into packet data
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
index a64183c2..eef9c7d0 100644
--- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
@@ -70,6 +70,16 @@ class ZoneActor(zone : Zone) extends Actor {
case msg @ Zone.Vehicle.Despawn =>
zone.Transport forward msg
+ //frwd to Projector actor
+ case msg @ Zone.HotSpot.Activity =>
+ zone.Activity forward msg
+
+ case msg @ Zone.HotSpot.UpdateNow =>
+ zone.Activity forward msg
+
+ case msg @ Zone.HotSpot.ClearAll =>
+ zone.Activity forward msg
+
//own
case Zone.Lattice.RequestSpawnPoint(zone_number, player, spawn_group) =>
if(zone_number == zone.Number) {
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala
new file mode 100644
index 00000000..f5052bd7
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala
@@ -0,0 +1,267 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.zones
+
+import akka.actor.{Actor, ActorRef, Cancellable}
+import net.psforever.objects.DefaultCancellable
+import net.psforever.types.PlanetSideEmpire
+import services.ServiceManager
+
+import scala.concurrent.duration._
+
+/**
+ * Manage hotspot information for a given zone,
+ * keeping track of aggressive faction interactions,
+ * and maintaining the visibility state of the hotspots that alert of the location of that activity.
+ * @param zone the zone
+ */
+class ZoneHotSpotProjector(zone : Zone) extends Actor {
+ /** a hook for the `GalaxyService` used to broadcast messages */
+ private var galaxy : ActorRef = ActorRef.noSender
+ /** the timer for the blanking process */
+ private var blanking : Cancellable = DefaultCancellable.obj
+ /** how long to wait in between blanking periods while hotspots decay */
+ private val blankingDelay : FiniteDuration = 15 seconds
+
+ private[this] val log = org.log4s.getLogger(s"${zone.Id.capitalize}HotSpotProjector")
+
+ /**
+ * Actions that occur before this `Actor` is formally started.
+ * Request a hook for the `GalaxyService`.
+ * @see `ServiceManager`
+ * @see `ServiceManager.Lookup`
+ */
+ override def preStart() : Unit = {
+ super.preStart()
+ ServiceManager.serviceManager ! ServiceManager.Lookup("galaxy")
+ }
+
+ /**
+ * Actions that occur after this `Actor` is formally stopped.
+ * Cancel all future blanking actions and release the `GalaxyService` hook.
+ */
+ override def postStop() : Unit = {
+ blanking.cancel
+ galaxy = ActorRef.noSender
+ super.postStop()
+ }
+
+ def receive : Receive = Initializing
+
+ /**
+ * Accept the `GalaxyService` hook and switch to active message processing when it arrives.
+ * @see `ActorContext.become`
+ * @see `ServiceManager`
+ * @see `ServiceManager.LookupResult`
+ * @see `ZoneHotSpotProjector.UpdateDurationFunction`
+ * @see `ZoneHotSpotProjector.UpdateMappingFunction`
+ * @return a partial function
+ */
+ def Initializing : Receive = {
+ case ServiceManager.LookupResult("galaxy", galaxyRef) =>
+ galaxy = galaxyRef
+ context.become(Established)
+
+ case ZoneHotSpotProjector.UpdateDurationFunction() =>
+ UpdateDurationFunction()
+
+ case ZoneHotSpotProjector.UpdateMappingFunction() =>
+ UpdateMappingFunction()
+
+ case _ =>
+ log.warn("not ready - still waiting on event system hook")
+ }
+
+ /**
+ * The active message processing message handler.
+ * @see `Zone.HotSpot.Activity`
+ * @see `Zone.HotSpot.ClearAll`
+ * @see `Zone.HotSpot.UpdateNow`
+ * @see `Zone.ActivityBy`
+ * @see `Zone.ActivityFor`
+ * @see `Zone.TryHotSpot`
+ * @see `ZoneHotSpotProjector.BlankingPhase`
+ * @see `ZoneHotSpotProjector.UpdateDurationFunction`
+ * @see `ZoneHotSpotProjector.UpdateMappingFunction`
+ * @return a partial function
+ */
+ def Established : Receive = {
+ case ZoneHotSpotProjector.UpdateDurationFunction() =>
+ blanking.cancel
+ UpdateDurationFunction()
+ UpdateHotSpots(PlanetSideEmpire.values, zone.HotSpots)
+ import scala.concurrent.ExecutionContext.Implicits.global
+ blanking = context.system.scheduler.scheduleOnce(blankingDelay, self, ZoneHotSpotProjector.BlankingPhase())
+
+ case ZoneHotSpotProjector.UpdateMappingFunction() =>
+ //remapped hotspots are produced from their `DisplayLocation` determined by the previous function
+ //this is different from the many individual activity locations that contributed to that `DisplayLocation`
+ blanking.cancel
+ UpdateMappingFunction()
+ UpdateHotSpots(PlanetSideEmpire.values, zone.HotSpots)
+ import scala.concurrent.ExecutionContext.Implicits.global
+ blanking = context.system.scheduler.scheduleOnce(blankingDelay, self, ZoneHotSpotProjector.BlankingPhase())
+
+ case Zone.HotSpot.Activity(defender, attacker, location) =>
+ log.trace(s"received information about activity in ${zone.Id}@$location")
+ val defenderFaction = defender.Faction
+ val attackerFaction = attacker.Faction
+ val noPriorHotSpots = zone.HotSpots.isEmpty
+ val duration = zone.HotSpotTimeFunction(defender, attacker)
+ if(duration.toNanos > 0) {
+ val hotspot = zone.TryHotSpot( zone.HotSpotCoordinateFunction(location) )
+ log.trace(s"updating activity status for ${zone.Id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}")
+ val noPriorActivity = !(hotspot.ActivityBy(defenderFaction) && hotspot.ActivityBy(attackerFaction))
+ //update the activity report for these factions
+ val affectedFactions = Seq(attackerFaction, defenderFaction)
+ affectedFactions.foreach { f =>
+ hotspot.ActivityFor(f) match {
+ case Some(events) =>
+ events.Duration = duration
+ events.Report()
+ case None => ;
+ }
+ }
+ //if the level of activity changed for one of the participants or the number of hotspots was zero
+ if(noPriorActivity || noPriorHotSpots) {
+ UpdateHotSpots(affectedFactions, zone.HotSpots)
+ if(noPriorHotSpots) {
+ import scala.concurrent.ExecutionContext.Implicits.global
+ blanking.cancel
+ blanking = context.system.scheduler.scheduleOnce(blankingDelay, self, ZoneHotSpotProjector.BlankingPhase())
+ }
+ }
+ }
+
+ case Zone.HotSpot.UpdateNow =>
+ log.trace(s"asked to update for zone ${zone.Id} without a blanking period or new activity")
+ UpdateHotSpots(PlanetSideEmpire.values, zone.HotSpots)
+
+ case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() =>
+ blanking.cancel
+ val curr : Long = System.nanoTime
+ //blanking dated activity reports
+ val changed = zone.HotSpots.flatMap(spot => {
+ spot.Activity.collect {
+ case (b, a) if a.LastReport + a.Duration.toNanos <= curr =>
+ a.Clear() //this faction has no more activity in this sector
+ (b, spot)
+ }
+ })
+ //collect and re-assign still-relevant hotspots
+ val spots = zone.HotSpots.filter(spot => {
+ spot.Activity
+ .values
+ .collect {
+ case a if a.Heat > 0 =>
+ true
+ }
+ .foldLeft(false)(_ || _)
+ })
+ val changesOnMap = zone.HotSpots.size - spots.size
+ log.trace(s"blanking out $changesOnMap hotspots from zone ${zone.Id}; ${spots.size} remain active")
+ zone.HotSpots = spots
+ //other hotspots still need to be blanked later
+ if(spots.nonEmpty) {
+ import scala.concurrent.ExecutionContext.Implicits.global
+ blanking.cancel
+ blanking = context.system.scheduler.scheduleOnce(blankingDelay, self, ZoneHotSpotProjector.BlankingPhase())
+ }
+ //if hotspots changed, redraw the remaining ones for the groups that changed
+ if(changed.nonEmpty && changesOnMap > 0) {
+ UpdateHotSpots(changed.map( { case (a : PlanetSideEmpire.Value, _) => a } ).toSet, spots)
+ }
+
+ case Zone.HotSpot.ClearAll() =>
+ log.trace(s"blanking out all hotspots from zone ${zone.Id} immediately")
+ blanking.cancel
+ zone.HotSpots = Nil
+ UpdateHotSpots(PlanetSideEmpire.values, Nil)
+
+ case _ => ;
+ }
+
+ /**
+ * Assign a new functionality for determining how long hotspots remain active.
+ * Recalculate all current hotspot information.
+ */
+ def UpdateDurationFunction(): Unit = {
+ zone.HotSpots.foreach { spot =>
+ spot.Activity.values.foreach { report =>
+ val heat = report.Heat
+ report.Clear()
+ report.Report(heat)
+ report.Duration = 0L
+ }
+ }
+ log.trace(s"new duration remapping function provided; reloading ${zone.HotSpots.size} hotspots for one blanking phase")
+ }
+
+ /**
+ * Assign new functionality for determining where to depict howspots on a given zone map.
+ * Recalculate all current hotspot information.
+ */
+ def UpdateMappingFunction() : Unit = {
+ val redoneSpots = zone.HotSpots.map { spot =>
+ val newSpot = new HotSpotInfo( zone.HotSpotCoordinateFunction(spot.DisplayLocation) )
+ PlanetSideEmpire.values.foreach { faction =>
+ if(spot.ActivityBy(faction)) {
+ newSpot.Activity(faction).Report( spot.Activity(faction).Heat )
+ newSpot.Activity(faction).Duration = spot.Activity(faction).Duration
+ }
+ }
+ newSpot
+ }
+ log.info(s"new coordinate remapping function provided; updating ${redoneSpots.size} hotspots")
+ zone.HotSpots = redoneSpots
+ }
+
+ /**
+ * Submit new updates regarding the hotspots for a given group (faction) in this zone.
+ * As per how the client operates, all previous hotspots not represented in this list will be erased.
+ * @param affectedFactions the factions whose hotspots for this zone need to be redrawn;
+ * if empty, no update/redraw calls are generated
+ * @param hotSpotInfos the information for the current hotspots in this zone;
+ * if empty or contains no information for a selected group,
+ * that group's hotspots will be eliminated (blanked) as a result
+ */
+ def UpdateHotSpots(affectedFactions : Iterable[PlanetSideEmpire.Value], hotSpotInfos : List[HotSpotInfo]) : Unit = {
+ val zoneNumber = zone.Number
+ affectedFactions.foreach(faction =>
+ galaxy ! Zone.HotSpot.Update(
+ faction,
+ zoneNumber,
+ 1,
+ ZoneHotSpotProjector.SpecificHotSpotInfo(faction, hotSpotInfos)
+ )
+ )
+ }
+
+ def CreateHotSpotUpdate(faction : PlanetSideEmpire.Value, hotSpotInfos : List[HotSpotInfo]) : List[HotSpotInfo] = {
+ Nil
+ }
+}
+
+object ZoneHotSpotProjector {
+ /**
+ * Reload the current hotspots for one more blanking phase.
+ */
+ final case class UpdateDurationFunction()
+ /**
+ * Reload the current hotspots by directly mapping the current ones to new positions.
+ */
+ final case class UpdateMappingFunction()
+ /**
+ * The internal message for eliminating hotspot data whose lifespan has exceeded its set duration.
+ */
+ private case class BlankingPhase()
+
+ /**
+ * Extract related hotspot activity information based on association with a faction.
+ * @param faction the faction
+ * @param hotSpotInfos the total activity information
+ * @return the discovered activity information that aligns with `faction`
+ */
+ def SpecificHotSpotInfo(faction : PlanetSideEmpire.Value, hotSpotInfos : List[HotSpotInfo]) : List[HotSpotInfo] = {
+ hotSpotInfos.filter { spot => spot.ActivityBy(faction) }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
index bcbe55c6..201de8eb 100644
--- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
@@ -25,6 +25,7 @@ import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObjectB
* `LoadMapMessage`
*/
class ZoneMap(private val name : String) {
+ private var scale : MapScale = MapScale.Dim8192
private var localObjects : List[ServerObjectBuilder[_]] = List()
private var linkTurretWeapon : Map[Int, Int] = Map()
private var linkTerminalPad : Map[Int, Int] = Map()
@@ -35,6 +36,13 @@ class ZoneMap(private val name : String) {
def Name : String = name
+ def Scale : MapScale = scale
+
+ def Scale_=(dim : MapScale) : MapScale = {
+ scale = dim
+ Scale
+ }
+
/**
* The list of all server object builder wrappers that have been assigned to this `ZoneMap`.
* @return the `List` of all `ServerObjectBuilders` known to this `ZoneMap`
diff --git a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala
index f605363b..f0de9cba 100644
--- a/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/HotSpotUpdateMessage.scala
@@ -3,8 +3,10 @@ package net.psforever.packet.game
import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.Vector3
import scodec.Codec
import scodec.codecs._
+import shapeless.{::, HNil}
/**
* Information for positioning a hotspot on the continental map.
@@ -27,16 +29,15 @@ final case class HotSpotInfo(x : Float,
* The hotspot system is an all-or-nothing affair.
* The received packet indicates the hotspots to display and the map will display only those hotspots.
* Inversely, if the received packet indicates no hotspots, the map will display no hotspots at all.
- * This "no hotspots" packet is always initially sent during zone setup during server login.
- * To clear away only some hotspots, but retains others, a continental `List` would have to be pruned selectively for the client.
+ * To clear away only some hotspots but retains others, a continental list would have to be pruned selectively for the client.
*
* Exploration:
* What does (zone) priority entail?
- * @param continent_id the zone
+ * @param zone_index the zone
* @param priority na
* @param spots a List of HotSpotInfo
*/
-final case class HotSpotUpdateMessage(continent_id : Int,
+final case class HotSpotUpdateMessage(zone_index : Int,
priority : Int,
spots : List[HotSpotInfo])
extends PlanetSideGamePacket {
@@ -60,8 +61,28 @@ object HotSpotInfo extends Marshallable[HotSpotInfo] {
object HotSpotUpdateMessage extends Marshallable[HotSpotUpdateMessage] {
implicit val codec : Codec[HotSpotUpdateMessage] = (
- ("continent_guid" | uint16L) ::
+ ("zone_index" | uint16L) ::
("priority" | uint4L) ::
("spots" | PacketHelpers.listOfNAligned(longL(12), 0, HotSpotInfo.codec))
- ).as[HotSpotUpdateMessage]
+ ).xmap[HotSpotUpdateMessage] (
+ {
+ case zone_index :: priority :: spots :: HNil =>
+ HotSpotUpdateMessage(zone_index, priority, spots)
+ },
+ {
+ case HotSpotUpdateMessage(zone_index, priority, spots) if spots.size > 4095 =>
+ //maximum number of points is 4095 (12-bit integer) but provided list of hotspot information is greater
+ //focus on depicting the "central" 4095 points only
+ val size = spots.size
+ val center = Vector3(
+ spots.foldLeft(0f)(_ + _.x) / size,
+ spots.foldLeft(0f)(_ + _.y) / size,
+ 0
+ )
+ zone_index :: priority :: spots.sortBy(spot => Vector3.DistanceSquared(Vector3(spot.x, spot.y, 0), center)).take(4095) :: HNil
+
+ case HotSpotUpdateMessage(zone_index, priority, spots) =>
+ zone_index :: priority :: spots :: HNil
+ }
+ )
}
diff --git a/common/src/main/scala/services/galaxy/GalaxyResponse.scala b/common/src/main/scala/services/galaxy/GalaxyResponse.scala
index 42ece3a5..980399ce 100644
--- a/common/src/main/scala/services/galaxy/GalaxyResponse.scala
+++ b/common/src/main/scala/services/galaxy/GalaxyResponse.scala
@@ -1,10 +1,12 @@
// Copyright (c) 2017 PSForever
package services.galaxy
+import net.psforever.objects.zones.HotSpotInfo
import net.psforever.packet.game.BuildingInfoUpdateMessage
object GalaxyResponse {
trait Response
+ final case class HotSpotUpdate(zone_id : Int, priority : Int, host_spot_info : List[HotSpotInfo]) extends Response
final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response
}
diff --git a/common/src/main/scala/services/galaxy/GalaxyService.scala b/common/src/main/scala/services/galaxy/GalaxyService.scala
index 262d64b4..796aee91 100644
--- a/common/src/main/scala/services/galaxy/GalaxyService.scala
+++ b/common/src/main/scala/services/galaxy/GalaxyService.scala
@@ -1,9 +1,9 @@
// Copyright (c) 2017 PSForever
package services.galaxy
-import akka.actor.{Actor, Props}
+import akka.actor.Actor
+import net.psforever.objects.zones.Zone
import net.psforever.packet.game.BuildingInfoUpdateMessage
-import services.local.support.{DoorCloseActor, HackClearActor}
import services.{GenericEventBus, Service}
class GalaxyService extends Actor {
@@ -15,8 +15,13 @@ class GalaxyService extends Actor {
val GalaxyEvents = new GenericEventBus[GalaxyServiceResponse]
- def receive = {
- // Service.Join requires a channel to be passed in normally but GalaxyService is an exception in that messages go to ALL connected players
+ def receive : Receive = {
+ case Service.Join(faction) if "TRNCVS".containsSlice(faction) =>
+ val path = s"/$faction/Galaxy"
+ val who = sender()
+ log.info(s"$who has joined $path")
+ GalaxyEvents.subscribe(who, path)
+
case Service.Join(_) =>
val path = s"/Galaxy"
val who = sender()
@@ -43,6 +48,12 @@ class GalaxyService extends Actor {
)
case _ => ;
}
+
+ case Zone.HotSpot.Update(faction, zone_num, priority, info) =>
+ GalaxyEvents.publish(
+ GalaxyServiceResponse(s"/$faction/Galaxy", GalaxyResponse.HotSpotUpdate(zone_num, priority, info))
+ )
+
case msg =>
log.info(s"Unhandled message $msg from $sender")
}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 93885272..8c3424ce 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -42,8 +42,9 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret}
import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _}
import net.psforever.objects.vital._
-import net.psforever.objects.zones.{InterstellarCluster, Zone}
+import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector}
import net.psforever.packet.game.objectcreate._
+import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo}
import net.psforever.types._
import services.{RemoverActor, vehicle, _}
import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
@@ -308,6 +309,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case GalaxyServiceResponse(_, reply) =>
reply match {
+ case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
+ sendResponse(
+ HotSpotUpdateMessage(
+ zone_index,
+ priority,
+ hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
+ )
+ )
case GalaxyResponse.MapUpdate(msg) =>
sendResponse(msg)
}
@@ -502,7 +511,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(ZoneInfoMessage(continentNumber, true, 0))
sendResponse(ZoneLockInfoMessage(continentNumber, false, true))
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
- sendResponse(HotSpotUpdateMessage(continentNumber, 1, Nil)) //normally set in bulk; should be fine doing per continent
+ sendResponse(HotSpotUpdateMessage(
+ continentNumber,
+ 1,
+ ZoneHotSpotProjector.SpecificHotSpotInfo(player.Faction, zone.HotSpots)
+ .map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
+ )) //normally set for all zones in bulk; should be fine manually updating per zone like this
case Zone.Population.PlayerHasLeft(zone, None) =>
log.info(s"$avatar does not have a body on ${zone.Id}")
@@ -520,7 +534,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Zone.Lattice.SpawnPoint(zone_id, spawn_tube) =>
var (pos, ori) = spawn_tube.SpecificPoint(continent.GUID(player.VehicleSeated) match {
- case Some(obj) =>
+ case Some(obj : Vehicle) if obj.Health > 0 =>
obj
case _ =>
player
@@ -790,6 +804,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatarService ! Service.Join(avatar.name) //channel will be player.Name
localService ! Service.Join(avatar.name) //channel will be player.Name
vehicleService ! Service.Join(avatar.name) //channel will be player.Name
+ galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
+ galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
@@ -990,7 +1006,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
//first damage entry -> most recent damage source -> killing blow
target.History.find(p => p.isInstanceOf[DamagingActivity]) match {
case Some(data : DamageFromProjectile) =>
- data.data.projectile.owner match {
+ val owner = data.data.projectile.owner
+ owner match {
case pSource : PlayerSource =>
continent.LivePlayers.find(_.Name == pSource.Name) match {
case Some(tplayer) =>
@@ -1001,6 +1018,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(DamageWithPositionMessage(damageToHealth + damageToArmor, vSource.Position))
case _ => ;
}
+ continent.Activity ! Zone.HotSpot.Activity(owner, data.Target, target.Position)
case _ => ;
}
}
@@ -2273,6 +2291,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
cargo.MountedIn = carrierGUID
hold.Occupant = cargo
+ cargo.Velocity = None
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)))
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)))
StartBundlingPackets()
@@ -2441,56 +2460,104 @@ class WorldSessionActor extends Actor with MDCContextAware {
def HandleVehicleDamageResolution(target : Vehicle) : Unit = {
val targetGUID = target.GUID
val playerGUID = player.GUID
- val continentId = continent.Id
val players = target.Seats.values.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
})
- if(target.Health > 0) {
- //alert occupants to damage source
- players.foreach(seat => {
- val tplayer = seat.Occupant.get
- avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(playerGUID, tplayer.GUID))
- })
- }
- else {
- //alert to vehicle death (hence, occupants' deaths)
- players.foreach(seat => {
- val tplayer = seat.Occupant.get
- val tplayerGUID = tplayer.GUID
- avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
- avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
- })
- //vehicle wreckage has no weapons
- target.Weapons.values
- .filter {
- _.Equipment.nonEmpty
+ target.LastShot match { //TODO: collision damage from/in history
+ case Some(shot) =>
+ if(target.Health > 0) {
+ //activity on map
+ continent.Activity ! Zone.HotSpot.Activity(shot.target, shot.projectile.owner, shot.hit_pos)
+ //alert occupants to damage source
+ HandleVehicleDamageAwareness(target, playerGUID, shot)
}
- .foreach(slot => {
- val wep = slot.Equipment.get
- avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID))
- })
- target.CargoHolds.values.foreach(hold => {
- hold.Occupant match {
- case Some(cargo) =>
+ else {
+ //alert to vehicle death (hence, occupants' deaths)
+ HandleVehicleDestructionAwareness(target, shot)
+ }
+ vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health))
+ vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields))
+ case None => ;
+ }
+ }
- case None => ;
- }
- })
- target.Definition match {
- case GlobalDefinitions.ams =>
- target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
- case GlobalDefinitions.router =>
- target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
- BeforeUnloadVehicle(target)
- localService ! LocalServiceMessage(continent.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), target, None))
- case _ => ;
+ /**
+ * na
+ * @param target na
+ * @param attribution na
+ * @param lastShot na
+ */
+ def HandleVehicleDamageAwareness(target : Vehicle, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = {
+ //alert occupants to damage source
+ target.Seats.values.filter(seat => {
+ seat.isOccupied && seat.Occupant.get.isAlive
+ }).foreach(seat => {
+ val tplayer = seat.Occupant.get
+ avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(attribution, tplayer.GUID))
+ })
+ //alert cargo occupants to damage source
+ target.CargoHolds.values.foreach(hold => {
+ hold.Occupant match {
+ case Some(cargo) =>
+ cargo.Health = 0
+ cargo.Shields = 0
+ cargo.History(lastShot)
+ HandleVehicleDamageAwareness(cargo, attribution, lastShot)
+ case None => ;
}
- avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, target.Position))
- vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent))
- vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute)))
+ })
+ }
+
+ /**
+ * na
+ * @param target na
+ * @param attribution na
+ * @param lastShot na
+ */
+ def HandleVehicleDestructionAwareness(target : Vehicle, lastShot : ResolvedProjectile) : Unit = {
+ val playerGUID = player.GUID
+ val continentId = continent.Id
+ //alert to vehicle death (hence, occupants' deaths)
+ target.Seats.values.filter(seat => {
+ seat.isOccupied && seat.Occupant.get.isAlive
+ }).foreach(seat => {
+ val tplayer = seat.Occupant.get
+ val tplayerGUID = tplayer.GUID
+ avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
+ avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
+ })
+ //vehicle wreckage has no weapons
+ target.Weapons.values
+ .filter {
+ _.Equipment.nonEmpty
+ }
+ .foreach(slot => {
+ val wep = slot.Equipment.get
+ avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID))
+ })
+ target.CargoHolds.values.foreach(hold => {
+ hold.Occupant match {
+ case Some(cargo) =>
+ cargo.Health = 0
+ cargo.Shields = 0
+ cargo.Position += Vector3.z(1)
+ cargo.History(lastShot) //necessary to kill cargo vehicle occupants //TODO: collision damage
+ HandleVehicleDestructionAwareness(cargo, lastShot) //might cause redundant packets
+ case None => ;
+ }
+ })
+ target.Definition match {
+ case GlobalDefinitions.ams =>
+ target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
+ case GlobalDefinitions.router =>
+ target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
+ BeforeUnloadVehicle(target)
+ localService ! LocalServiceMessage(continent.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), target, None))
+ case _ => ;
}
- vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health))
- vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields))
+ avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(target.GUID, playerGUID, playerGUID, target.Position))
+ vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent))
+ vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute)))
}
/**
@@ -2932,7 +2999,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
localService ! Service.Join(factionOnContinentChannel)
vehicleService ! Service.Join(continentId)
vehicleService ! Service.Join(factionOnContinentChannel)
- galaxyService ! Service.Join("galaxy")
configZone(continent)
sendResponse(TimeOfDayMessage(1191182336))
//custom
@@ -3265,11 +3331,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
obj.Position = pos
obj.Orientation = ang
- obj.Velocity = vel
- if(obj.Definition.CanFly) {
- obj.Flying = flight.nonEmpty //usually Some(7)
+ if(obj.MountedIn.isEmpty) {
+ obj.Velocity = vel
+ if(obj.Definition.CanFly) {
+ obj.Flying = flight.nonEmpty //usually Some(7)
+ }
+ vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
}
- vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
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
@@ -6668,6 +6736,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}) match {
case Some(shot) =>
+ continent.Activity ! Zone.HotSpot.Activity(pentry, shot.projectile.owner, shot.hit_pos)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to))
case None =>
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(pentry, pentry, 0))
@@ -7011,8 +7080,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
else {
continent.GUID(player.VehicleSeated) match {
- case Some(_ : Vehicle) =>
- cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 12) //warp gates
+ case Some(obj : Vehicle) if obj.Health > 0 =>
+ cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 12) //warp gates for functioning vehicles
case None =>
cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) //player character spawns
case _ =>
diff --git a/pslogin/src/main/scala/Zones.scala b/pslogin/src/main/scala/Zones.scala
index 15dc2b0a..eeb7da93 100644
--- a/pslogin/src/main/scala/Zones.scala
+++ b/pslogin/src/main/scala/Zones.scala
@@ -6,10 +6,13 @@ import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.zones.Zone
import net.psforever.types.PlanetSideEmpire
+
object Zones {
val z1 = new Zone("z1", Maps.map1, 1) {
override def Init(implicit context : ActorContext) : Unit = {
super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
BuildingByMapId(1).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
BuildingByMapId(2).get.asInstanceOf[WarpGate].BroadcastFor = PlanetSideEmpire.TR
@@ -18,13 +21,27 @@ object Zones {
}
}
- val z2 = new Zone("z2", Maps.map2, 2)
+ val z2 = new Zone("z2", Maps.map2, 2) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val z3 = new Zone("z3", Maps.map3, 3)
+ val z3 = new Zone("z3", Maps.map3, 3) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
val z4 = new Zone("z4", Maps.map4, 4) {
override def Init(implicit context : ActorContext) : Unit = {
super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
Buildings.values.flatMap {
_.Amenities.collect {
@@ -97,11 +114,19 @@ object Zones {
}
}
- val z5 = new Zone("z5", Maps.map5, 5)
+ val z5 = new Zone("z5", Maps.map5, 5) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
val z6 = new Zone("z6", Maps.map6, 6) {
override def Init(implicit context : ActorContext) : Unit = {
super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
GUID(2094) match {
case Some(silo : ResourceSilo) =>
@@ -121,13 +146,37 @@ object Zones {
}
}
- val z7 = new Zone("z7", Maps.map7, 7)
+ val z7 = new Zone("z7", Maps.map7, 7) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val z8 = new Zone("z8", Maps.map8, 8)
+ val z8 = new Zone("z8", Maps.map8, 8) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val z9 = new Zone("z9", Maps.map9, 9)
+ val z9 = new Zone("z9", Maps.map9, 9) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val z10 = new Zone("z10", Maps.map10, 10)
+ val z10 = new Zone("z10", Maps.map10, 10) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
val home1 = new Zone("home1", Maps.map11, 11){
override def Init(implicit context : ActorContext) : Unit = {
@@ -180,25 +229,85 @@ object Zones {
val tzcovs = new Zone("tzcovs", Maps.map16, 22)
- val c1 = new Zone("c1", Maps.ugd01, 23)
+ val c1 = new Zone("c1", Maps.ugd01, 23) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val c2 = new Zone("c2", Maps.ugd02, 24)
+ val c2 = new Zone("c2", Maps.ugd02, 24) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val c3 = new Zone("c3", Maps.ugd03, 25)
+ val c3 = new Zone("c3", Maps.ugd03, 25) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val c4 = new Zone("c4", Maps.ugd04, 26)
+ val c4 = new Zone("c4", Maps.ugd04, 26) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val c5 = new Zone("c5", Maps.ugd05, 27)
+ val c5 = new Zone("c5", Maps.ugd05, 27) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val c6 = new Zone("c6", Maps.ugd06, 28)
+ val c6 = new Zone("c6", Maps.ugd06, 28) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val i1 = new Zone("i1", Maps.map99, 29)
+ val i1 = new Zone("i1", Maps.map99, 29) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val i2 = new Zone("i2", Maps.map98, 30)
+ val i2 = new Zone("i2", Maps.map98, 30) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val i3 = new Zone("i3", Maps.map97, 31)
+ val i3 = new Zone("i3", Maps.map97, 31) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
- val i4 = new Zone("i4", Maps.map96, 32)
+ val i4 = new Zone("i4", Maps.map96, 32) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+ HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
+ HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
+ }
+ }
/**
* Get the zone identifier name for the sanctuary continent of a given empire.
@@ -255,4 +364,100 @@ object Zones {
0
}
}
+
+ object HotSpots {
+ import net.psforever.objects.ballistics.SourceEntry
+ import net.psforever.objects.zones.MapScale
+ import net.psforever.types.Vector3
+
+ import scala.concurrent.duration._
+
+ /**
+ * Produce hotspot coordinates based on map coordinates.
+ * @see `FindClosestDivision`
+ * @param scale the map's scale (width and height)
+ * @param longDivNum the number of division lines spanning the width of the `scale`
+ * @param latDivNum the number of division lines spanning the height of the `scale`
+ * @param pos the absolute position of the activity reported
+ * @return the position for a hotspot
+ */
+ def StandardRemapping(scale : MapScale, longDivNum : Int, latDivNum : Int)(pos : Vector3) : Vector3 = {
+ Vector3(
+ //x
+ FindClosestDivision(pos.x, scale.width, longDivNum),
+ //y
+ FindClosestDivision(pos.y, scale.height, latDivNum),
+ //z is always zero - maps are flat 2D planes
+ 0
+ )
+ }
+
+ /**
+ * Produce hotspot coordinates based on map coordinates.
+ *
+ * Transform a reported number by mapping it
+ * into a division from a regular pattern of divisions
+ * defined by the scale divided evenly a certain number of times.
+ * The depicted number of divisions is actually one less than the parameter number
+ * as the first division is used to represent everything before that first division (there is no "zero").
+ * Likewise, the last division occurs before the farther edge of the scale is counted
+ * and is used to represent everything after that last division.
+ * This is not unlike rounding.
+ * @param coordinate the point to scale
+ * @param scale the map's scale (width and height)
+ * @param divisions the number of division lines spanning across the `scale`
+ * @return the closest regular division
+ */
+ private def FindClosestDivision(coordinate : Float, scale : Float, divisions : Float) : Float = {
+ val divLength : Float = scale / divisions
+ if(coordinate >= scale - divLength) {
+ scale - divLength
+ }
+ else if(coordinate >= divLength) {
+ val sector : Float = (coordinate * divisions / scale).toInt * divLength
+ val nextSector : Float = sector + divLength
+ if(coordinate - sector < nextSector - coordinate) {
+ sector
+ }
+ else {
+ nextSector
+ }
+ }
+ else {
+ divLength
+ }
+ }
+
+ /**
+ * Determine a duration for which the hotspot will be displayed on the zone map.
+ * Friendly fire is not recognized.
+ * @param defender the defending party
+ * @param attacker the attacking party
+ * @return the duration
+ */
+ def StandardTimeRules(defender : SourceEntry, attacker : SourceEntry) : FiniteDuration = {
+ import net.psforever.objects.ballistics._
+ import net.psforever.objects.GlobalDefinitions
+ if(attacker.Faction == defender.Faction) {
+ 0 seconds
+ }
+ else {
+ //TODO is target occupy-able and occupied, or jammer-able and jammered?
+ defender match {
+ case _ : PlayerSource =>
+ 60 seconds
+ case _ : VehicleSource =>
+ 60 seconds
+ case t : ObjectSource if t.Definition == GlobalDefinitions.manned_turret =>
+ 60 seconds
+ case _ : DeployableSource =>
+ 30 seconds
+ case _ : ComplexDeployableSource =>
+ 30 seconds
+ case _ =>
+ 0 seconds
+ }
+ }
+ }
+ }
}
diff --git a/pslogin/src/main/scala/csr/CSRZoneImpl.scala b/pslogin/src/main/scala/csr/CSRZoneImpl.scala
index 595b7f63..9fc33c3e 100644
--- a/pslogin/src/main/scala/csr/CSRZoneImpl.scala
+++ b/pslogin/src/main/scala/csr/CSRZoneImpl.scala
@@ -53,13 +53,13 @@ object CSRZoneImpl {
"home3" -> CSRZoneImpl("VS Sanctuary", "map13", "home3"),
"tzshtr" -> CSRZoneImpl("VR Shooting Range TR", "map14", "tzshtr"),
"tzdrtr" -> CSRZoneImpl("VR Driving Range TR", "map15", "tzdrtr"),
- "tzcotr" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl TR", "map16", "tzcotr"),
+ "tzcotr" -> CSRZoneImpl("VR Combat Zone TR", "map16", "tzcotr"),
"tzshvs" -> CSRZoneImpl("VR Shooting Range VS", "map14", "tzshvs"),
"tzdrvs" -> CSRZoneImpl("VR Driving Range VS", "map15", "tzdrvs"),
- "tzcovs" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl VS", "map16", "tzcovs"),
+ "tzcovs" -> CSRZoneImpl("VR Combat Zone VS", "map16", "tzcovs"),
"tzshnc" -> CSRZoneImpl("VR Shooting Range NC", "map14", "tzshnc"),
"tzdrnc" -> CSRZoneImpl("VR Driving Range NC", "map15", "tzdrnc"),
- "tzconc" -> CSRZoneImpl("VR Combat csr.CSRZoneImpl NC", "map16", "tzconc"),
+ "tzconc" -> CSRZoneImpl("VR Combat Zone NC", "map16", "tzconc"),
"c1" -> CSRZoneImpl("Supai", "ugd01", "c1"),
"c2" -> CSRZoneImpl("Hunhau", "ugd02", "c2"),
"c3" -> CSRZoneImpl("Adlivun", "ugd03", "c3"),
diff --git a/pslogin/src/main/scala/zonemaps/Map96.scala b/pslogin/src/main/scala/zonemaps/Map96.scala
index 8809277e..375a80b0 100644
--- a/pslogin/src/main/scala/zonemaps/Map96.scala
+++ b/pslogin/src/main/scala/zonemaps/Map96.scala
@@ -10,12 +10,13 @@ import net.psforever.objects.serverobject.structures.{Building, FoundationBuilde
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.zones.ZoneMap
+import net.psforever.objects.zones.{MapScale, ZoneMap}
import net.psforever.types.Vector3
object Map96 {
// Nexus
val ZoneMap = new ZoneMap("map96") {
+ Scale = MapScale.Dim4096
Building1()
diff --git a/pslogin/src/main/scala/zonemaps/Map97.scala b/pslogin/src/main/scala/zonemaps/Map97.scala
index 6dc18362..5f972516 100644
--- a/pslogin/src/main/scala/zonemaps/Map97.scala
+++ b/pslogin/src/main/scala/zonemaps/Map97.scala
@@ -10,12 +10,13 @@ import net.psforever.objects.serverobject.structures.{Building, FoundationBuilde
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.zones.ZoneMap
+import net.psforever.objects.zones.{MapScale, ZoneMap}
import net.psforever.types.Vector3
object Map97 {
// Desolation
val ZoneMap = new ZoneMap("map97") {
+ Scale = MapScale.Dim4096
Building11()
diff --git a/pslogin/src/main/scala/zonemaps/Map98.scala b/pslogin/src/main/scala/zonemaps/Map98.scala
index 7b6c6f04..f398b7bb 100644
--- a/pslogin/src/main/scala/zonemaps/Map98.scala
+++ b/pslogin/src/main/scala/zonemaps/Map98.scala
@@ -11,12 +11,13 @@ import net.psforever.objects.serverobject.structures.{Building, FoundationBuilde
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.zones.ZoneMap
+import net.psforever.objects.zones.{MapScale, ZoneMap}
import net.psforever.types.Vector3
object Map98 {
// Ascension
val ZoneMap = new ZoneMap("map98") {
+ Scale = MapScale.Dim4096
Building39()
diff --git a/pslogin/src/main/scala/zonemaps/Map99.scala b/pslogin/src/main/scala/zonemaps/Map99.scala
index e74ca72e..d615d7dd 100644
--- a/pslogin/src/main/scala/zonemaps/Map99.scala
+++ b/pslogin/src/main/scala/zonemaps/Map99.scala
@@ -11,12 +11,13 @@ import net.psforever.objects.serverobject.structures.{Building, FoundationBuilde
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, ProximityTerminal, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.zones.ZoneMap
+import net.psforever.objects.zones.{MapScale, ZoneMap}
import net.psforever.types.Vector3
object Map99 {
// Extinction
val ZoneMap = new ZoneMap("map99") {
+ Scale = MapScale.Dim4096
Building7()