From 1fb35bacd1064845f4d5671dae8921e001c472ff Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Wed, 21 Aug 2019 10:40:29 -0400 Subject: [PATCH] Hotspots (#260) * basic information processing actor for projecting hotspot information back to the clients * added measurements to the continents to keep track of their coordinate-sizes; corrected an old regex issue affecting some (few) CSR zone names * started on zone storage for active hot spots * moved (Zone)HotSpotProjector onto the top of each of the zones and adjusting the GalaxyService workflow; added a map blanking routine for outdated hotspots; different damage targets cause different amounts of hotpot lifespan (test) * separated hotspots by the faction that should have those specific hotspots depicted; hard cap on the number of entries that can be added to the packet HotSpotUpdateMessage to correspond to the capacity of the size field; increased hotspot sector mapping (64 -> 80) and lowered the blanking period delay (30 -> 15) * adding comments to the majority of classes involved in hotspot management; moved hotspot coordination mapping and duration selection functions into the zone itself rather than the projector (though calls will still be made to perform updates to the existing projectiles through the projector); the formal continents have been assigned some default hotspot functionality * rebase, plus 'galaxy' channel name * capped the amount of heat a hotspot can accumulate to avoid overflow; vehicles being damaged now generate hotspots; corrected a long-standing issue involving cargo vehicles when the ferrying vehicle is destroyed * from > to < --- .../psforever/objects/zones/HotSpotInfo.scala | 162 +++++++++++ .../psforever/objects/zones/MapScale.scala | 19 ++ .../net/psforever/objects/zones/Zone.scala | 85 +++++- .../psforever/objects/zones/ZoneActor.scala | 10 + .../objects/zones/ZoneHotSpotProjector.scala | 267 ++++++++++++++++++ .../net/psforever/objects/zones/ZoneMap.scala | 8 + .../packet/game/HotSpotUpdateMessage.scala | 33 ++- .../services/galaxy/GalaxyResponse.scala | 2 + .../scala/services/galaxy/GalaxyService.scala | 19 +- .../src/main/scala/WorldSessionActor.scala | 177 ++++++++---- pslogin/src/main/scala/Zones.scala | 239 ++++++++++++++-- pslogin/src/main/scala/csr/CSRZoneImpl.scala | 6 +- pslogin/src/main/scala/zonemaps/Map96.scala | 3 +- pslogin/src/main/scala/zonemaps/Map97.scala | 3 +- pslogin/src/main/scala/zonemaps/Map98.scala | 3 +- pslogin/src/main/scala/zonemaps/Map99.scala | 3 +- 16 files changed, 949 insertions(+), 90 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/MapScale.scala create mode 100644 common/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala 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()