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()