mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
763 lines
31 KiB
Scala
763 lines
31 KiB
Scala
// Copyright (c) 2022 PSForever
|
|
package net.psforever.services
|
|
|
|
import akka.actor.{ActorRef, Cancellable}
|
|
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
|
|
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
|
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
|
import net.psforever.actors.session.SessionActor
|
|
import net.psforever.actors.zone.{BuildingActor, ZoneActor}
|
|
import net.psforever.actors.zone.building.WarpGateLogic
|
|
import net.psforever.objects.Default
|
|
import net.psforever.objects.serverobject.structures.{Building, WarpGate}
|
|
import net.psforever.objects.zones.Zone
|
|
import net.psforever.packet.game.ChatMsg
|
|
import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse}
|
|
import net.psforever.types.ChatMessageType
|
|
import net.psforever.util.Config
|
|
import net.psforever.zones.Zones
|
|
|
|
import scala.concurrent.duration._
|
|
|
|
object CavernRotationService {
|
|
val CavernRotationServiceKey: ServiceKey[Command] =
|
|
ServiceKey[CavernRotationService.Command](id = "cavernRotationService")
|
|
|
|
def apply(): Behavior[Command] =
|
|
Behaviors
|
|
.supervise[Command] {
|
|
Behaviors.withStash(100) { buffer =>
|
|
Behaviors.setup { context =>
|
|
context.system.receptionist ! Receptionist.Register(CavernRotationServiceKey, context.self)
|
|
new CavernRotationService(context, buffer).start()
|
|
}
|
|
}
|
|
}.onFailure[Exception](SupervisorStrategy.restart)
|
|
|
|
sealed trait Command
|
|
|
|
private case class ServiceManagerLookupResult(result: ServiceManager.LookupResult) extends Command
|
|
|
|
final case class ManageCaverns(zones: Iterable[Zone]) extends Command
|
|
|
|
final case class SendCavernRotationUpdates(sendToSession: ActorRef) extends Command
|
|
|
|
final case class LockedZoneUpdate(zone: Zone, timeUntilUnlock: Long)
|
|
|
|
final case class UnlockedZoneUpdate(zone: Zone)
|
|
|
|
sealed trait HurryRotation extends Command {
|
|
def zoneid: String
|
|
}
|
|
|
|
case object HurryNextRotation extends HurryRotation { def zoneid = "" }
|
|
|
|
final case class HurryRotationToZoneLock(zoneid: String) extends HurryRotation
|
|
|
|
final case class HurryRotationToZoneUnlock(zoneid: String) extends HurryRotation
|
|
|
|
final case class ReportRotationOrder(sendToSession: ActorRef) extends Command
|
|
|
|
private case object SwitchZone extends Command
|
|
|
|
private case class ClosingWarning(counter: Int) extends Command
|
|
|
|
/**
|
|
* A token designed to keep track of the managed cavern zone.
|
|
* @param zone the zone
|
|
*/
|
|
class ZoneMonitor(val zone: Zone) {
|
|
/** is the zone currently accessible */
|
|
var locked: Boolean = true
|
|
/** when did the timer start (ms) */
|
|
var start: Long = 0L
|
|
/** for how long does the timer go on (ms) */
|
|
var duration: Long = 0L
|
|
}
|
|
|
|
/**
|
|
* The periodic warning for when a cavern closes,
|
|
* usually announcing fifteen, ten, then five minutes before closure.
|
|
* @see `ChatMsg`
|
|
* @see `GalaxyService`
|
|
* @param zone zone monitor
|
|
* @param counter current time until closure
|
|
* @param galaxyService callback to display the warning;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
* @return `true`, if the zone was actually locked and the message was shown;
|
|
* `false`, otherwise
|
|
*/
|
|
private def closedCavernWarning(zone: ZoneMonitor, counter: Int, galaxyService: ActorRef): Boolean = {
|
|
if (!zone.locked) {
|
|
galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(
|
|
ChatMsg(ChatMessageType.UNK_229, s"@cavern_closing_warning^@${zone.zone.id}~^@$counter~")
|
|
))
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the cavern zones lattice links
|
|
* for cavern access.
|
|
* @param zones the cavern zones being configured
|
|
*/
|
|
private def activateLatticeLinksAndWarpGateAccessibility(zones: Seq[Zone]): Unit = {
|
|
val sortedZones = zones.sortBy(_.Number)
|
|
establishLatticeLinksForUnlockedCaverns(sortedZones)
|
|
sortedZones.foreach { zone =>
|
|
openZoneWarpGateAccessibility(zone)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply the lattice links that connect geowarp gates and cavern warp gates to the lattices for each zone.
|
|
* Separate the connection entry strings,
|
|
* locate each individual zone and warp gate in that zone,
|
|
* then add one to the other's lattice connectivity on the fly.
|
|
* @param zones the cavern zones
|
|
*/
|
|
private def establishLatticeLinksForUnlockedCaverns(zones: Seq[Zone]): Unit = {
|
|
val key = s"caverns-${zones.map(_.id).mkString("-")}"
|
|
Zones.cavernLattice.get(key) match {
|
|
case Some(links) =>
|
|
links.foreach { link =>
|
|
val entryA = link.head
|
|
val entryB = link.last
|
|
val splitA = entryA.split("/")
|
|
val splitB = entryB.split("/")
|
|
((zones.find { _.id.equals(splitA.head) }, Zones.zones.find { _.id.equals(splitB.head) }) match {
|
|
case (Some(zone1), Some(zone2)) => (zone1.Building(splitA.last), zone2.Building(splitB.last))
|
|
case _ => (None, None)
|
|
}) match {
|
|
case (Some(gate1), Some(gate2)) =>
|
|
gate1.Zone.AddIntercontinentalLatticeLink(gate1, gate2)
|
|
gate2.Zone.AddIntercontinentalLatticeLink(gate2, gate1)
|
|
case _ => ;
|
|
}
|
|
}
|
|
case _ =>
|
|
org.log4s.getLogger("CavernRotationService").error(s"can not find mapping to open $key")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect all of the warp gates in a (cavern) zone and the adjacent building along the lattice
|
|
* and update the connectivity of the gate pairs
|
|
* so that the gate pair is active and broadcasts correctly.
|
|
* @param zone the zone
|
|
* @return all of the affected warp gates
|
|
*/
|
|
private def openZoneWarpGateAccessibility(zone: Zone): Iterable[WarpGate] = {
|
|
findZoneWarpGatesForChangingAccessibility(zone).map { case (wg, otherWg, building) =>
|
|
wg.Active = true
|
|
otherWg.Active = true
|
|
wg.Actor ! BuildingActor.AlertToFactionChange(building)
|
|
otherWg.Zone.actor ! ZoneActor.ZoneMapUpdate()
|
|
wg
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the cavern zones lattice links
|
|
* for cavern closures.
|
|
* @param zones the cavern zones being configured
|
|
*/
|
|
private def disableLatticeLinksAndWarpGateAccessibility(zones: Seq[Zone]): Unit = {
|
|
val sortedZones = zones.sortBy(_.Number)
|
|
sortedZones.foreach { zone =>
|
|
closeZoneWarpGateAccessibility(zone)
|
|
}
|
|
revokeLatticeLinksForUnlockedCaverns(sortedZones)
|
|
}
|
|
|
|
/**
|
|
* Disconnect the lattice links that connect geowarp gates and cavern warp gates to the lattices for each zone.
|
|
* Separate the connection entry strings,
|
|
* locate each individual zone and warp gate in that zone,
|
|
* then remove one from the other's lattice connectivity on the fly.
|
|
* @param zones the cavern zones
|
|
*/
|
|
private def revokeLatticeLinksForUnlockedCaverns(zones: Seq[Zone]): Unit = {
|
|
val key = s"caverns-${zones.map(_.id).mkString("-")}"
|
|
Zones.cavernLattice.get(key) match {
|
|
case Some(links) =>
|
|
links.foreach { link =>
|
|
val entryA = link.head
|
|
val entryB = link.last
|
|
val splitA = entryA.split("/")
|
|
val splitB = entryB.split("/")
|
|
((zones.find { _.id.equals(splitA.head) }, Zones.zones.find { _.id.equals(splitB.head) }) match {
|
|
case (Some(zone1), Some(zone2)) => (zone1.Building(splitA.last), zone2.Building(splitB.last))
|
|
case _ => (None, None)
|
|
}) match {
|
|
case (Some(gate1), Some(gate2)) =>
|
|
gate1.Zone.RemoveIntercontinentalLatticeLink(gate1, gate2)
|
|
gate2.Zone.RemoveIntercontinentalLatticeLink(gate2, gate1)
|
|
case _ => ;
|
|
}
|
|
}
|
|
case _ =>
|
|
org.log4s.getLogger("CavernRotationService").error(s"can not find mapping to close $key")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect all of the warp gates in a (cavern) zone and the adjacent building along the lattice
|
|
* and update the connectivity of the gate pairs
|
|
* so that the gate pair is inactive and stops broadcasting.
|
|
* @param zone the zone
|
|
* @return all of the affected warp gates
|
|
*/
|
|
private def closeZoneWarpGateAccessibility(zone: Zone): Iterable[WarpGate] = {
|
|
findZoneWarpGatesForChangingAccessibility(zone).map { case (wg, otherWg, building) =>
|
|
wg.Active = false
|
|
otherWg.Active = false
|
|
wg.Actor ! BuildingActor.AlertToFactionChange(building)
|
|
otherWg.Zone.actor ! ZoneActor.ZoneMapUpdate()
|
|
//must trigger the connection test from the other side to equalize
|
|
WarpGateLogic.findNeighborhoodNormalBuilding(otherWg.Neighbours.getOrElse(Nil)) match {
|
|
case Some(b) => otherWg.Actor ! BuildingActor.AlertToFactionChange(b)
|
|
case None => ;
|
|
}
|
|
wg
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Within a given zone, find:
|
|
* (1) all warp gates;
|
|
* (2) the warp gates that are adjacent along the intercontinental lattice (in the other zone); and,
|
|
* (3) the facility building that is adjacent to the warp gate (in this zone).
|
|
* Will be using the recovered grouping for manipulation of the intercontinental lattice extending from the zone.
|
|
* @param zone the zone
|
|
* @return the triples
|
|
*/
|
|
private def findZoneWarpGatesForChangingAccessibility(zone: Zone): Iterable[(WarpGate, WarpGate, Building)] = {
|
|
zone.Buildings.values
|
|
.collect {
|
|
case wg: WarpGate =>
|
|
val neighborhood = wg.AllNeighbours.getOrElse(Nil)
|
|
(
|
|
WarpGateLogic.findNeighborhoodWarpGate(neighborhood),
|
|
WarpGateLogic.findNeighborhoodNormalBuilding(neighborhood)
|
|
) match {
|
|
case (Some(otherWg: WarpGate), Some(building)) =>
|
|
Some(wg, otherWg, building)
|
|
case _ =>
|
|
None
|
|
}
|
|
}.flatten
|
|
}
|
|
|
|
/**
|
|
* Take two zone monitors and swap the order of the zones.
|
|
* Keep the timers from each other the same.
|
|
* @param list the ordered zone monitors
|
|
* @param to index of one zone monitor
|
|
* @param from index of another zone monitor
|
|
*/
|
|
private def swapMonitors(list: List[ZoneMonitor], to: Int, from: Int): Unit = {
|
|
val toMonitor = list(to)
|
|
val fromMonitor = list(from)
|
|
list.updated(to, new ZoneMonitor(fromMonitor.zone) {
|
|
locked = toMonitor.locked
|
|
start = toMonitor.start
|
|
duration = toMonitor.duration
|
|
})
|
|
list.updated(from, new ZoneMonitor(toMonitor.zone) {
|
|
locked = fromMonitor.locked
|
|
start = fromMonitor.start
|
|
duration = fromMonitor.duration
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A service that assists routine access to a series of game zones
|
|
* through the manipulation of connections between transmit point structures.<br>
|
|
* <br>
|
|
* The caverns were a group of game zones that were intended to be situated underground.
|
|
* Access to the caverns was only sometimes possible
|
|
* through the use of special above-ground warp gates called geowarps (geowarp gates)
|
|
* and those geowarps were not always functional.
|
|
* Usually, two caverns were available at a time and connections to these caverns were fixed
|
|
* to specific active geowarp gates.
|
|
* The changing availability of the caverns through the change of geowarp gate activity
|
|
* was colloquially referred to as a "rotation" since it followed a predictable cycle.
|
|
* The cycle was not just one of time but one of route
|
|
* as one specific geowarp gates would open to the same destination cavern.<br>
|
|
* <br>
|
|
* The client controls warp gate destinations.
|
|
* The server can only confirm those destinations.
|
|
* The connectivity of a geowarp gate to a cavern warp gate had to have been determined
|
|
* by opening the cavern with an appropriate packet
|
|
* and checking the map description of the cavern gates.
|
|
* The description text explains which of the geowarp gates in whichever zone has been connected; and,
|
|
* where usually static and inanimate, that geowarp gate will bubble online and begin to rotate
|
|
* and have a complementary destination map description.
|
|
* Opening different combinations of caverns changes the destination these warp gate pairs will connect
|
|
* and not always being connected at all.
|
|
* The warp gate pairs for the cavern connections must be re-evaluated for each combination and with each rotation
|
|
* and all relevant pairings must be defined in advance.
|
|
* @see `ActorContext`
|
|
* @see `Building`
|
|
* @see `ChatMsg`
|
|
* @see `Config.app.game.cavernRotation`
|
|
* @see `GalaxyService`
|
|
* @see `GalaxyAction.LockedZoneUpdate`
|
|
* @see `GalaxyResponse.UnlockedZoneUpdate`
|
|
* @see `InterstellarClusterService`
|
|
* @see `org.log4s.getLogger`
|
|
* @see `resources/zonemaps/lattice.json`
|
|
* @see `SessionActor`
|
|
* @see `SessionActor.SendResponse`
|
|
* @see `StashBuffer`
|
|
* @see `WarpGate`
|
|
* @see `Zone`
|
|
* @see `ZoneForcedCavernConnectionsMessage`
|
|
* @see `ZoneInfoMessage`
|
|
*/
|
|
//TODO currently, can only support any 1 cavern unlock order and the predetermined 2 cavern unlock order
|
|
class CavernRotationService(
|
|
context: ActorContext[CavernRotationService.Command],
|
|
buffer: StashBuffer[CavernRotationService.Command]
|
|
) {
|
|
import CavernRotationService._
|
|
|
|
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
|
|
"galaxy",
|
|
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
|
|
)
|
|
|
|
/** monitors for the cavern zones */
|
|
var managedZones: List[ZoneMonitor] = Nil
|
|
/** index of the next cavern that will lock */
|
|
var nextToLock: Int = 0
|
|
/** index of the next cavern that will unlock */
|
|
var nextToUnlock: Int = 0
|
|
/** timer for cavern rotation - the cavern closing warning */
|
|
var lockTimer: Cancellable = Default.Cancellable
|
|
/** timer for cavern rotation - the actual opening and closing functionality */
|
|
var unlockTimer: Cancellable = Default.Cancellable
|
|
var simultaneousUnlockedZones: Int = Config.app.game.cavernRotation.simultaneousUnlockedZones
|
|
/** time between individual cavern rotation events (hours) */
|
|
val timeBetweenRotationsHours: Float = Config.app.game.cavernRotation.hoursBetweenRotation
|
|
/** number of zones unlocked at the same time */
|
|
/** period of all caverns having rotated (hours) */
|
|
var timeToCompleteAllRotationsHours: Float = 0f
|
|
/** how long before any given cavern closure that the first closing message is shown (minutes) */
|
|
val firstClosingWarningAtMinutes: Int = 15
|
|
|
|
def start(): Behavior[CavernRotationService.Command] = {
|
|
Behaviors.receiveMessage {
|
|
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
|
|
request match {
|
|
case "galaxy" =>
|
|
buffer.unstashAll(active(endpoint))
|
|
case _ =>
|
|
Behaviors.same
|
|
}
|
|
|
|
case other =>
|
|
buffer.stash(other)
|
|
Behaviors.same
|
|
}
|
|
}
|
|
|
|
def active(galaxyService: ActorRef): Behavior[CavernRotationService.Command] = {
|
|
Behaviors.receiveMessage {
|
|
case ManageCaverns(zones) =>
|
|
manageCaverns(zones.toSeq)
|
|
Behaviors.same
|
|
|
|
case ClosingWarning(counter)
|
|
if counter == 15 || counter == 10 =>
|
|
if (CavernRotationService.closedCavernWarning(managedZones(nextToLock), counter, galaxyService)) {
|
|
val next = counter - 5
|
|
lockTimerToDisplayWarning(next.minutes, next)
|
|
}
|
|
Behaviors.same
|
|
|
|
case ClosingWarning(counter) =>
|
|
CavernRotationService.closedCavernWarning(managedZones(nextToLock), counter, galaxyService)
|
|
Behaviors.same
|
|
|
|
case ReportRotationOrder(sendToSession) =>
|
|
reportRotationOrder(sendToSession)
|
|
Behaviors.same
|
|
|
|
case SwitchZone =>
|
|
zoneRotationFunc(galaxyService)
|
|
Behaviors.same
|
|
|
|
case HurryNextRotation =>
|
|
hurryNextRotation(galaxyService)
|
|
Behaviors.same
|
|
|
|
case HurryRotationToZoneLock(zoneid) =>
|
|
hurryRotationToZoneLock(zoneid, galaxyService)
|
|
Behaviors.same
|
|
|
|
case HurryRotationToZoneUnlock(zoneid) =>
|
|
hurryRotationToZoneUnlock(zoneid, galaxyService)
|
|
Behaviors.same
|
|
|
|
case SendCavernRotationUpdates(sendToSession) =>
|
|
sendCavernRotationUpdates(sendToSession)
|
|
Behaviors.same
|
|
|
|
case _ =>
|
|
Behaviors.same
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param zones the zones for submission
|
|
* @return `true`, if the setup has been completed;
|
|
* `false`, otherwise
|
|
*/
|
|
def manageCaverns(zones: Seq[Zone]): Boolean = {
|
|
if (managedZones.isEmpty) {
|
|
val onlyCaverns = zones.filter{ z => z.map.cavern }
|
|
val collectedZones = Config.app.game.cavernRotation.enhancedRotationOrder match {
|
|
case Nil => onlyCaverns
|
|
case list => list.flatMap { index => onlyCaverns.find(_.Number == index ) }
|
|
}
|
|
if (collectedZones.nonEmpty) {
|
|
simultaneousUnlockedZones = math.min(simultaneousUnlockedZones, collectedZones.size)
|
|
managedZones = collectedZones.map(zone => new ZoneMonitor(zone)).toList
|
|
val rotationSize = managedZones.size
|
|
timeToCompleteAllRotationsHours = rotationSize.toFloat * timeBetweenRotationsHours
|
|
val curr = System.currentTimeMillis()
|
|
val fullDurationAsHours = timeToCompleteAllRotationsHours.hours
|
|
val fullDurationAsMillis = fullDurationAsHours.toMillis
|
|
val startingInThePast = curr - fullDurationAsMillis
|
|
val (unlockedZones, lockedZones) = managedZones.splitAt(simultaneousUnlockedZones)
|
|
var i = 0
|
|
//the timer data in all zone monitors
|
|
(lockedZones ++ unlockedZones).foreach { zone =>
|
|
i += 1
|
|
zone.locked = true
|
|
zone.start = startingInThePast + (i * timeBetweenRotationsHours).hours.toMillis
|
|
zone.duration = fullDurationAsMillis
|
|
}
|
|
//unlocked zones
|
|
unlockedZones.foreach { zone =>
|
|
zone.locked = false
|
|
}
|
|
CavernRotationService.activateLatticeLinksAndWarpGateAccessibility(unlockedZones.map(_.zone))
|
|
nextToLock = 0
|
|
lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes)
|
|
//locked zones ...
|
|
nextToUnlock = simultaneousUnlockedZones
|
|
unlockTimerToSwitchZone(timeBetweenRotationsHours.hours)
|
|
//println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString(""))
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param sendToSession callback reference
|
|
*/
|
|
def reportRotationOrder(sendToSession: ActorRef): Unit = {
|
|
val zoneStates = managedZones.collect {
|
|
case zone =>
|
|
if (zone.locked) {
|
|
s"<${zone.zone.id}>"
|
|
} else {
|
|
s"${zone.zone.id}"
|
|
}
|
|
}.mkString(" ")
|
|
sendToSession ! SessionActor.SendResponse(
|
|
ChatMsg(ChatMessageType.UNK_229, s"[$zoneStates]")
|
|
)
|
|
Behaviors.same
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @see `GalaxyService`
|
|
* @param zoneid zone to lock next
|
|
* @param galaxyService callback to update the server and clients;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
* @return `true`, if the target zone is locked when complete;
|
|
* `false`, otherwise
|
|
*/
|
|
def hurryRotationToZoneLock(zoneid: String, galaxyService: ActorRef): Boolean = {
|
|
//TODO currently, can only switch for 1 active cavern
|
|
if (simultaneousUnlockedZones == 1) {
|
|
if ((nextToLock until nextToLock + simultaneousUnlockedZones)
|
|
.map { i => managedZones(i % managedZones.size) }
|
|
.indexWhere { _.zone.id.equals(zoneid) } match {
|
|
case -1 =>
|
|
false
|
|
case 0 =>
|
|
true
|
|
case index =>
|
|
CavernRotationService.swapMonitors(managedZones, nextToLock, index)
|
|
true
|
|
}) {
|
|
hurryNextRotation(galaxyService, forcedRotationOverride=true)
|
|
}
|
|
true
|
|
} else {
|
|
org.log4s.getLogger("CavernRotationService").warn(s"can not alter cavern order")
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @see `GalaxyService`
|
|
* @param zoneid zone to unlock next
|
|
* @param galaxyService callback to update the server and clients;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
* @return `true`, if the target zone is unlocked when complete;
|
|
* `false`, otherwise
|
|
*/
|
|
def hurryRotationToZoneUnlock(zoneid: String, galaxyService: ActorRef): Boolean = {
|
|
//TODO currently, can only switch for 1 active cavern
|
|
if (simultaneousUnlockedZones == 1) {
|
|
if (managedZones(nextToUnlock).zone.id.equals(zoneid)) {
|
|
hurryNextRotation(galaxyService, forcedRotationOverride = true)
|
|
true
|
|
} else {
|
|
managedZones.indexWhere { z => z.zone.id.equals(zoneid) } match {
|
|
case -1 =>
|
|
false //not found
|
|
case index if nextToLock <= index && index < nextToUnlock + simultaneousUnlockedZones =>
|
|
true //already unlocked
|
|
case index =>
|
|
CavernRotationService.swapMonitors(managedZones, nextToUnlock, index)
|
|
hurryNextRotation(galaxyService, forcedRotationOverride = true)
|
|
true
|
|
}
|
|
}
|
|
} else {
|
|
org.log4s.getLogger("CavernRotationService").error(s"can not alter cavern order")
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param sendToSession callback reference
|
|
*/
|
|
def sendCavernRotationUpdates(sendToSession: ActorRef): Unit = {
|
|
val curr = System.currentTimeMillis()
|
|
val (lockedZones, unlockedZones) = managedZones.partition(_.locked)
|
|
//borrow GalaxyService response structure, but send to the specific endpoint math.max(0, monitor.start + monitor.duration - curr)
|
|
unlockedZones.foreach { monitor =>
|
|
sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone))
|
|
}
|
|
val sortedLocked = lockedZones.sortBy(z => z.start)
|
|
sortedLocked.take(2).foreach { monitor =>
|
|
sendToSession ! GalaxyServiceResponse(
|
|
"",
|
|
GalaxyResponse.LockedZoneUpdate(monitor.zone, math.max(0, monitor.start + monitor.duration - curr))
|
|
)
|
|
}
|
|
sortedLocked.takeRight(2).foreach { monitor =>
|
|
sendToSession ! GalaxyServiceResponse(
|
|
"",
|
|
GalaxyResponse.LockedZoneUpdate(monitor.zone, 0L)
|
|
)
|
|
}
|
|
}
|
|
|
|
def sendCavernRotationUpdatesToAll(galaxyService: ActorRef): Unit = {
|
|
val curr = System.currentTimeMillis()
|
|
val (lockedZones, unlockedZones) = managedZones.partition(_.locked)
|
|
unlockedZones.foreach { z =>
|
|
galaxyService ! GalaxyServiceMessage(GalaxyAction.UnlockedZoneUpdate(z.zone))
|
|
}
|
|
val sortedLocked = lockedZones.sortBy(z => z.start)
|
|
sortedLocked.take(2).foreach { z =>
|
|
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(z.zone, z.start + z.duration - curr))
|
|
}
|
|
sortedLocked.takeRight(2).foreach { z =>
|
|
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(z.zone, 0L))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Progress to the next significant cavern rotation event.<br>
|
|
* <br>
|
|
* If the time until the next rotation is greater than the time where the cavern closing warning would be displayed,
|
|
* progress to that final cavern closing warning.
|
|
* Adjust the timing for that advancement.
|
|
* If the final cavern closing warning was already displayed,
|
|
* just perform the cavern rotation.
|
|
* @see `GalaxyService`
|
|
* @param galaxyService callback to update the server and clients;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
* @param forcedRotationOverride force a cavern rotation in a case where a closing warning would be displayed instead
|
|
*/
|
|
def hurryNextRotation(
|
|
galaxyService: ActorRef,
|
|
forcedRotationOverride: Boolean = false
|
|
): Unit = {
|
|
val curr = System.currentTimeMillis() //ms
|
|
val unlocking = managedZones(nextToUnlock)
|
|
val timeToNextClosingEvent = unlocking.start + unlocking.duration - curr //ms
|
|
val fiveMinutes = 5.minutes //minutes duration
|
|
if (
|
|
forcedRotationOverride || Config.app.game.cavernRotation.forceRotationImmediately ||
|
|
timeToNextClosingEvent < fiveMinutes.toMillis
|
|
) {
|
|
//zone transition immediately
|
|
lockTimer.cancel()
|
|
unlockTimer.cancel()
|
|
retimeZonesUponForcedRotation(galaxyService)
|
|
zoneRotationFunc(galaxyService)
|
|
lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes)
|
|
} else {
|
|
//instead of transitioning immediately, jump to the 5 minute rotation warning for the benefit of players
|
|
lockTimer.cancel() //won't need to retime until zone change
|
|
CavernRotationService.closedCavernWarning(managedZones(nextToLock), counter=5, galaxyService)
|
|
unlockTimerToSwitchZone(fiveMinutes)
|
|
retimeZonesUponForcedAdvancement(timeToNextClosingEvent.milliseconds - fiveMinutes, galaxyService)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually perform zone rotation as determined by the managed zone monitors and the timers.<br>
|
|
* <br>
|
|
* The process of zone rotation occurs by having a zone that is determined to be closing
|
|
* and a zone that is determied to be opening
|
|
* and a potential series of zones "in between" the two that are also open.
|
|
* All of the currently opened zones are locked and the zone to be permanently closed is forgotten.
|
|
* The zone that should be opening is added to the aforementioned sequence of zones
|
|
* and then the zones in that sequence are opened.
|
|
* The zones that would otherwise be unaffected by a single zone opening and a single cone closing must be affected
|
|
* because the cavern gates will not connect to the same geowarp gates with the change in the sequence.
|
|
* After the rotation, the indices to the next closing zone and next opening zone are updated.
|
|
* Modifying the zone monitor timekeeping and the actual timers and the indices are the easy parts.
|
|
* @see `GalaxyService`
|
|
* @param galaxyService callback to update the server and clients;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
*/
|
|
def zoneRotationFunc(
|
|
galaxyService: ActorRef
|
|
): Unit = {
|
|
val curr = System.currentTimeMillis()
|
|
val locking = managedZones(nextToLock)
|
|
val unlocking = managedZones(nextToUnlock)
|
|
val lockingZone = locking.zone
|
|
val unlockingZone = unlocking.zone
|
|
val fullHoursBetweenRotationsAsHours = timeToCompleteAllRotationsHours.hours
|
|
val fullHoursBetweenRotationsAsMillis = fullHoursBetweenRotationsAsHours.toMillis
|
|
val hoursBetweenRotationsAsHours = timeBetweenRotationsHours.hours
|
|
val prevToLock = nextToLock
|
|
nextToLock = (nextToLock + 1) % managedZones.size
|
|
nextToUnlock = (nextToUnlock + 1) % managedZones.size
|
|
//this zone will be locked; open when the timer runs out
|
|
locking.locked = true
|
|
locking.start = curr
|
|
unlockTimerToSwitchZone(hoursBetweenRotationsAsHours)
|
|
//this zone will be unlocked; alert the player that it will lock soon when the timer runs out
|
|
unlocking.locked = false
|
|
unlocking.start = curr
|
|
lockTimerToDisplayWarning(hoursBetweenRotationsAsHours - firstClosingWarningAtMinutes.minutes)
|
|
//alert clients to change
|
|
if (lockingZone ne unlockingZone) {
|
|
galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(
|
|
ChatMsg(ChatMessageType.UNK_229, s"@cavern_switched^@${lockingZone.id}~^@${unlockingZone.id}")
|
|
))
|
|
//change warp gate statuses to reflect zone lock state
|
|
CavernRotationService.disableLatticeLinksAndWarpGateAccessibility(
|
|
((prevToLock until managedZones.size) ++ (0 until prevToLock))
|
|
.take(simultaneousUnlockedZones)
|
|
.map(managedZones(_).zone)
|
|
)
|
|
CavernRotationService.activateLatticeLinksAndWarpGateAccessibility(
|
|
((nextToLock until managedZones.size) ++ (0 until nextToLock))
|
|
.take(simultaneousUnlockedZones)
|
|
.map(managedZones(_).zone)
|
|
)
|
|
}
|
|
sendCavernRotationUpdatesToAll(galaxyService)
|
|
}
|
|
|
|
/**
|
|
* If the zones are forced to rotate before the timer would normally complete,
|
|
* correct all of the zone monitors to give the impression of the rotation that occurred.
|
|
* Only affect the backup parameters of the timers that are maintained by the zone monitors.
|
|
* Do not actually affect the functional timers.
|
|
* @see `GalaxyService`
|
|
* @param galaxyService callback to update the zone timers;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
*/
|
|
def retimeZonesUponForcedRotation(galaxyService: ActorRef) : Unit = {
|
|
val curr = System.currentTimeMillis()
|
|
val rotationSize = managedZones.size
|
|
val fullDurationAsMillis = timeToCompleteAllRotationsHours.hours.toMillis
|
|
val startingInThePast = curr - fullDurationAsMillis
|
|
//this order allows the monitors to be traversed in order of ascending time to unlock
|
|
(0 +: ((nextToUnlock until rotationSize) ++ (0 until nextToUnlock)))
|
|
.zipWithIndex
|
|
.drop(1)
|
|
.foreach { case (monitorIndex, index) =>
|
|
val zone = managedZones(monitorIndex)
|
|
val newStart = startingInThePast + (index * timeBetweenRotationsHours).hours.toMillis
|
|
zone.start = newStart
|
|
}
|
|
//println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString(""))
|
|
}
|
|
|
|
/**
|
|
* If the natural process of switching between caverns is hurried,
|
|
* advance the previous start time of each zone monitor to give the impression of the hastened rotation.
|
|
* This does not actually affect the functional timers
|
|
* nor is it in response to an actual zone rotation event.
|
|
* It only affects the backup parameters of the timers that are maintained by the zone monitors.
|
|
* @see `GalaxyService`
|
|
* @param advanceTimeBy amount of time advancement
|
|
* @param galaxyService callback to update the zone timers;
|
|
* should be the reference to `GalaxyService`, hence the literal name
|
|
*/
|
|
def retimeZonesUponForcedAdvancement(
|
|
advanceTimeBy: FiniteDuration,
|
|
galaxyService: ActorRef
|
|
) : Unit = {
|
|
val curr = System.currentTimeMillis()
|
|
val advanceByTimeAsMillis = advanceTimeBy.toMillis
|
|
managedZones.foreach { zone =>
|
|
zone.start = zone.start - advanceByTimeAsMillis
|
|
}
|
|
sendCavernRotationUpdatesToAll(galaxyService)
|
|
//println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString(""))
|
|
}
|
|
|
|
/**
|
|
* Update the timer for the cavern closing message.
|
|
* @param duration new time until message display
|
|
* @param counter the counter that indicates the next message to display
|
|
*/
|
|
def lockTimerToDisplayWarning(
|
|
duration: FiniteDuration,
|
|
counter: Int = firstClosingWarningAtMinutes
|
|
): Unit = {
|
|
lockTimer.cancel()
|
|
lockTimer = context.scheduleOnce(duration, context.self, ClosingWarning(counter))
|
|
}
|
|
|
|
/**
|
|
* Update the timer for the zone switching process.
|
|
* @param duration new time until switching
|
|
*/
|
|
def unlockTimerToSwitchZone(duration: FiniteDuration): Unit = {
|
|
unlockTimer.cancel()
|
|
unlockTimer = context.scheduleOnce(duration, context.self, SwitchZone)
|
|
}
|
|
}
|