* 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 <
This commit is contained in:
Fate-JH 2019-08-21 10:40:29 -04:00 committed by GitHub
parent 2048fa19cb
commit 1fb35bacd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 949 additions and 90 deletions

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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.<br>
@ -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:<br>
@ -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

View file

@ -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) {

View file

@ -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) }
}
}

View file

@ -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`

View file

@ -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.<br>
@ -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.<br>
* To clear away only some hotspots but retains others, a continental list would have to be pruned selectively for the client.<br>
* <br>
* Exploration:<br>
* 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
}
)
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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 _ =>

View file

@ -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.<br>
* <br>
* 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
}
}
}
}
}

View file

@ -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"),

View file

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

View file

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

View file

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

View file

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