Intercontinental Gaslighting (#998)

* diversified building management by injecting behavior; allocated entries for the intercontinental lattice and have begun connecting warp gate entities along the intercontinental lattice; beginnings of warp gate broadcast operations; disabled free merit commendations

* allow transit across a predetermined warp gate path, i.e., proper zone-to-zone gating

* game variables for modifying warp gate behaviors; moved choice of building game logic into overloaded constructor; only handle the capitol fore dome in more realistic conditions; warp gate state restored primarily b y internal game logic; changes to which and how gates are declared inactive or broadcast at startup

* initial work on WarpgateLinkOverrideMessage, even if the packet doesn't seem to do anything; added basic service for rotating cavern zone locks via the galaxy messaging service; moved error checking for lattice connectedness

* cavern closing warning messages queued

* starting to set up ChatActor for /zlock command, and added /setbaseresources; conditions for correcting broadcast conditions of a locking warp gate pair; system for rotating through locking and unlocking cavern zones only uses two timers now and has an advance command that speeds to the next closing warning or cavern opening

* expedited cavern rotations available via '/zonerotate' and '!zonerotate [-list]'; '/zonelock' should work for caverns, though distorting the rotation order to accommodate the cavern being unlocked; configuration arguments exist for the setup of cavern rotations and for the rotation itself

* populated cavern lattice connections for a specific rotation order; warp gates will properly activate and deactivate and modify their neighborhood information based on which stage of the rotation; fed up with the blockmap going wrong; added a sanity test for the cavern lattice; Spiker damage calculation changes

* adjusted local variable requirements of BuildingActor to integrate retained actors more closely with the Behavior; on the other hand, another value is passed around the logic

* bug fixes observed from issues found in logs since 20220520; halved the spawn height when gating to a cavern warpgate

* cavern benefits are now represented by enumeration classes rather than additive binary numbers; when facilities change state, benefits are evaluated; when caverns rotate, benefits are evaluated; cavern facility logic added; attempted handling for inventory disarray conditions (untested)

* broke down tabs for easier navigation; added test to stop spawning of cavern equipment when not otherwise permitted

* code comments, everywhere; correcting issues with cavern rotation timing reports

* but is it flying?
This commit is contained in:
Fate-JH 2022-06-14 02:21:24 -04:00 committed by GitHub
parent 546a4e4f0d
commit ced228509c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 4445 additions and 1235 deletions

View file

@ -25,7 +25,7 @@ import net.psforever.services.chat.ChatService
import net.psforever.services.galaxy.GalaxyService import net.psforever.services.galaxy.GalaxyService
import net.psforever.services.properties.PropertyOverrideManager import net.psforever.services.properties.PropertyOverrideManager
import net.psforever.services.teamwork.SquadService import net.psforever.services.teamwork.SquadService
import net.psforever.services.{InterstellarClusterService, ServiceManager} import net.psforever.services.{CavernRotationService, InterstellarClusterService, ServiceManager}
import net.psforever.util.Config import net.psforever.util.Config
import net.psforever.zones.Zones import net.psforever.zones.Zones
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@ -120,10 +120,6 @@ object Server {
} }
val zones = Zones.zones ++ Seq(Zone.Nowhere) val zones = Zones.zones ++ Seq(Zone.Nowhere)
system.spawn(ChatService(), ChatService.ChatServiceKey.id)
system.spawn(InterstellarClusterService(zones), InterstellarClusterService.InterstellarClusterServiceKey.id)
val serviceManager = ServiceManager.boot val serviceManager = ServiceManager.boot
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary") serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy") serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy")
@ -132,6 +128,10 @@ object Server {
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager") serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager")
serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart") serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart")
system.spawn(CavernRotationService(), CavernRotationService.CavernRotationServiceKey.id)
system.spawn(InterstellarClusterService(zones), InterstellarClusterService.InterstellarClusterServiceKey.id)
system.spawn(ChatService(), ChatService.ChatServiceKey.id)
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket")
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")

View file

@ -110,6 +110,43 @@ game {
standard_armor, standard_armor,
agile_armor agile_armor
] ]
warp-gates {
# When a gating fails, fall back to sanctuary rather than stay in the same zone
default-to-sanctuary-destination = yes
# When a facility next to a warp gate is captured by one faction,
# if the facility on the other side of the intercontinental gate pair is owned by a different faction,
# that gate pair becomes a broadcast warp gate for those factions
broadcast-between-conflicted-factions = yes
}
cavern-rotation = {
# The number of hours between any given cavern locking and another cavern unlocking,
# not the total number of hours between a single cavern locking and unlocking.
hours-between-rotation = 3
# How many caverns are unlocked at once during rotations
# Pay attention to the logs that a corresponding combinational existence found in zonemaps/lattice.json
# Examples:
# [a,b,c] with 1 requires 'caverns-a' 'caverns-b' 'caverns-c'
# [a,b,c] with 2 requires 'caverns-a-b' 'caverns-b-c' 'caverns-a-c'
# [a,b,c] with 3 requires 'caverns-a-b-c'
# [a,b,c,d] with 3 requires 'caverns-a-b-c' 'caverns-b-c-d' 'caverns-a-c-d' 'caverns-a-b-d'
simultaneous-unlocked-zones = 2
# A list of zone numbers that correspond to the caverns, in a continuous order
# in which the caverns are locked and are unlocked.
# When left empty, the order of the caverns is traversed as-is provided
# For example, for cavern zones with number [23, 24, 25, 26, 27, 28],
# the order [23, 24, 26, 23, 24, 25, 26] would eliminate #27 and #28 from the rotation
enhanced-rotation-order = [23, 24, 25, 26, 27, 28]
# If a cavern rotation is forced by the system,
# the system will attempt to advance only to the first possible closing warning message at five minutes
# When set, however, the next zone unlock is carried out regardless of the amount of time remaining
force-rotation-immediately = false
}
} }
anti-cheat { anti-cheat {

View file

@ -1027,7 +1027,7 @@
"Map99_Gate_Three" "Map99_Gate_Three"
] ]
], ],
"udg01": [ "ugd01": [
[ [
"N_Redoubt", "N_Redoubt",
"N_ATPlant" "N_ATPlant"
@ -1077,7 +1077,7 @@
"S_ATPlant" "S_ATPlant"
] ]
], ],
"udg02": [ "ugd02": [
[ [
"N_Redoubt", "N_Redoubt",
"N_ATPlant" "N_ATPlant"
@ -1127,7 +1127,7 @@
"S_ATPlant" "S_ATPlant"
] ]
], ],
"udg03": [ "ugd03": [
[ [
"NW_Redoubt", "NW_Redoubt",
"NW_ATPlant" "NW_ATPlant"
@ -1177,7 +1177,7 @@
"SE_ATPlant" "SE_ATPlant"
] ]
], ],
"udg04": [ "ugd04": [
[ [
"N_Redoubt", "N_Redoubt",
"N_ATPlant" "N_ATPlant"
@ -1227,7 +1227,7 @@
"S_ATPlant" "S_ATPlant"
] ]
], ],
"udg05": [ "ugd05": [
[ [
"NW_Redoubt", "NW_Redoubt",
"NW_ATPlant" "NW_ATPlant"
@ -1281,7 +1281,7 @@
"SE_ATPlant" "SE_ATPlant"
] ]
], ],
"udg06": [ "ugd06": [
[ [
"N_Redoubt", "N_Redoubt",
"N_ATPlant" "N_ATPlant"
@ -1330,5 +1330,423 @@
"GW_Cavern6_S", "GW_Cavern6_S",
"S_ATPlant" "S_ATPlant"
] ]
],
"intercontinental": [
[
"z1/WG_Solsar_to_Amerish",
"home2/WG_TRSanc_to_Cyssor"
],
[
"z1/WG_Solsar_to_Cyssor",
"z3/WG_Cyssor_to_NCSanc"
],
[
"z1/WG_Solsar_to_Hossin",
"z6/WG_Ceryshen_to_Forseral"
],
[
"z1/WG_Solsar_to_Forseral",
"i3/Map97_Gate_One"
],
[
"z2/WG_Hossin_to_VSSanc",
"home2/WG_TRSanc_to_Forseral"
],
[
"z2/WG_Hossin_to_Solsar",
"z4/WG_Ishundar_to_Searhus"
],
[
"z2/WG_Hossin_to_Ceryshen",
"z10/WG_Amerish_to_Ceryshen"
],
[
"z2/WG_Hossin_to_Oshur",
"z9/WG_Searhus_to_Ishundar"
],
[
"z3/WG_Cyssor_to_Searhus",
"z7/WG_Esamir_to_Oshur"
],
[
"z3/WG_Cyssor_to_Solsar",
"z4/WG_Ishundar_to_VSSanc"
],
[
"z3/WG_Cyssor_to_TRSanc",
"z6/WG_Ceryshen_to_Amerish"
],
[
"z4/WG_Ishundar_to_TRSanc",
"z7/WG_Esamir_to_Searhus"
],
[
"z4/WG_Ishundar_to_Ceryshen",
"z5/WG_Forseral_to_Ceryshen"
],
[
"z5/WG_Forseral_to_TRSanc",
"i2/Map98_Gate_One"
],
[
"z5/WG_Forseral_to_Solsar",
"home3/WG_VSSanc_to_Hossin"
],
[
"z5/WG_Forseral_to_Oshur",
"z7/WG_Esamir_to_NCSanc"
],
[
"z6/WG_Ceryshen_to_Ishundar",
"z9/WG_Searhus_to_Esamir"
],
[
"z6/WG_Ceryshen_to_Hossin",
"home3/WG_VSSanc_to_Esamir"
],
[
"z7/WG_Esamir_to_VSSanc",
"home1/WG_NCSanc_to_Esamir"
],
[
"z9/WG_Searhus_to_Cyssor",
"z10/WG_Amerish_to_NCSanc"
],
[
"z10/WG_Amerish_to_Solsar",
"home1/WG_NCSanc_to_Amerish"
],
[
"z10/WG_Amerish_to_Oshur",
"i1/Map99_Gate_One"
],
[
"i1/Map99_Gate_Two",
"i3/Map97_Gate_Three"
],
[
"i2/Map98_Gate_Three",
"i3/Map97_Gate_Two"
],
[
"i4/Map96_Gate_One",
"i2/Map98_Gate_Two"
],
[
"i4/Map96_Gate_Two",
"i1/Map99_Gate_Three"
]
],
"caverns-c1": [
[
"c1/GW_Cavern1_N",
"z10/GW_Amerish_N"
],
[
"c1/GW_Cavern1_S",
"z5/GW_Forseral_N"
],
[
"c1/GW_Cavern1_E",
"z1/GW_Solsar_N"
],
[
"c1/GW_Cavern1_W",
"z4/GW_Ishundar_N"
]
],
"caverns-c2": [
[
"c2/GW_Cavern2_N",
"z5/GW_Forseral_N"
],
[
"c2/GW_Cavern2_S",
"z3/GW_Cyssor_S"
],
[
"c2/GW_Cavern2_E",
"z6/GW_Ceryshen_S"
],
[
"c2/GW_Cavern2_W",
"z7/GW_Esamir_S"
]
],
"caverns-c3": [
[
"c3/GW_Cavern3_N",
"z7/GW_Esamir_S"
],
[
"c3/GW_Cavern3_S",
"z2/GW_Hossin_N"
],
[
"c3/GW_Cavern3_E",
"z5/GW_Forseral_N"
],
[
"c3/GW_Cavern3_W",
"z9/GW_Searhus_S"
]
],
"caverns-c4": [
[
"c4/GW_Cavern4_N",
"z5/GW_Forseral_N"
],
[
"c4/GW_Cavern4_S",
"z10/GW_Amerish_N"
],
[
"c4/GW_Cavern4_E",
"z7/GW_Esamir_S"
],
[
"c4/GW_Cavern4_W",
"z3/GW_Cyssor_S"
]
],
"caverns-c5": [
[
"c5/GW_Cavern5_N",
"z2/GW_Hossin_N"
],
[
"c5/GW_Cavern5_S",
"z1/GW_Solsar_N"
],
[
"c5/GW_Cavern5_E",
"z3/GW_Cyssor_S"
],
[
"c5/GW_Cavern5_W",
"z9/GW_Searhus_N"
]
],
"caverns-c6": [
[
"c6/GW_Cavern6_N",
"z5/GW_Forseral_S"
],
[
"c6/GW_Cavern6_S",
"z2/GW_Hossin_S"
],
[
"c6/GW_Cavern6_E",
"z4/GW_Ishundar_S"
],
[
"c6/GW_Cavern6_W",
"z3/GW_Cyssor_S"
]
],
"caverns-c1-c2": [
[
"c1/GW_Cavern1_N",
"z9/GW_Searhus_N"
],
[
"c1/GW_Cavern1_S",
"z6/GW_Ceryshen_N"
],
[
"c1/GW_Cavern1_E",
"z1/GW_Solsar_S"
],
[
"c1/GW_Cavern1_W",
"z3/GW_Cyssor_N"
],
[
"c2/GW_Cavern2_N",
"z4/GW_Ishundar_S"
],
[
"c2/GW_Cavern2_S",
"z2/GW_Hossin_N"
],
[
"c2/GW_Cavern2_E",
"z7/GW_Esamir_S"
],
[
"c2/GW_Cavern2_W",
"z10/GW_Amerish_S"
]
],
"caverns-c2-c3": [
[
"c2/GW_Cavern2_N",
"z7/GW_Esamir_N"
],
[
"c2/GW_Cavern2_S",
"z4/GW_Ishundar_S"
],
[
"c2/GW_Cavern2_E",
"z3/GW_Cyssor_S"
],
[
"c2/GW_Cavern2_W",
"z2/GW_Hossin_N"
],
[
"c3/GW_Cavern3_N",
"z10/GW_Amerish_N"
],
[
"c3/GW_Cavern3_S",
"z1/GW_Solsar_N"
],
[
"c3/GW_Cavern3_E",
"z6/GW_Ceryshen_N"
],
[
"c3/GW_Cavern3_W",
"z5/GW_Forseral_S"
]
],
"caverns-c3-c4": [
[
"c3/GW_Cavern3_N",
"z6/GW_Ceryshen_N"
],
[
"c3/GW_Cavern3_S",
"z4/GW_Ishundar_S"
],
[
"c3/GW_Cavern3_E",
"z10/GW_Amerish_S"
],
[
"c3/GW_Cavern3_W",
"z2/GW_Hossin_S"
],
[
"c4/GW_Cavern4_N",
"z5/GW_Forseral_N"
],
[
"c4/GW_Cavern4_S",
"z7/GW_Esamir_S"
],
[
"c4/GW_Cavern4_E",
"z9/GW_Searhus_N"
],
[
"c4/GW_Cavern4_W",
"z3/GW_Cyssor_N"
]
],
"caverns-c4-c5": [
[
"c4/GW_Cavern4_N",
"z4/GW_Ishundar_N"
],
[
"c4/GW_Cavern4_S",
"z1/GW_Solsar_S"
],
[
"c4/GW_Cavern4_E",
"z3/GW_Cyssor_N"
],
[
"c4/GW_Cavern4_W",
"z2/GW_Hossin_N"
],
[
"c5/GW_Cavern5_N",
"z10/GW_Amerish_N"
],
[
"c5/GW_Cavern5_S",
"z6/GW_Ceryshen_N"
],
[
"c5/GW_Cavern5_E",
"z7/GW_Esamir_S"
],
[
"c5/GW_Cavern5_W",
"z9/GW_Searhus_N"
]
],
"caverns-c5-c6": [
[
"c5/GW_Cavern5_N",
"z10/GW_Amerish_S"
],
[
"c5/GW_Cavern5_S",
"z7/GW_Esamir_S"
],
[
"c5/GW_Cavern5_E",
"z2/GW_Hossin_S"
],
[
"c5/GW_Cavern5_W",
"z1/GW_Solsar_N"
],
[
"c6/GW_Cavern6_N",
"z5/GW_Forseral_N"
],
[
"c6/GW_Cavern6_S",
"z3/GW_Cyssor_N"
],
[
"c6/GW_Cavern6_E",
"z6/GW_Ceryshen_N"
],
[
"c6/GW_Cavern6_W",
"z9/GW_Searhus_N"
]
],
"caverns-c1-c6": [
[
"c1/GW_Cavern1_N",
"z3/GW_Cyssor_S"
],
[
"c1/GW_Cavern1_S",
"z1/GW_Solsar_S"
],
[
"c1/GW_Cavern1_E",
"z6/GW_Ceryshen_S"
],
[
"c1/GW_Cavern1_W",
"z5/GW_Forseral_N"
],
[
"c6/GW_Cavern6_N",
"z2/GW_Hossin_S"
],
[
"c6/GW_Cavern6_S",
"z10/GW_Amerish_N"
],
[
"c6/GW_Cavern6_E",
"z9/GW_Searhus_N"
],
[
"c6/GW_Cavern6_W",
"z4/GW_Ishundar_N"
]
] ]
} }

View file

@ -9,7 +9,7 @@ import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cos
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.{Default, Player, Session} import net.psforever.objects.{Default, Player, Session}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets} import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
import net.psforever.objects.zones.Zoning import net.psforever.objects.zones.Zoning
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage} import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
@ -18,9 +18,12 @@ import net.psforever.util.{Config, PointOfInterest}
import net.psforever.zones.Zones import net.psforever.zones.Zones
import net.psforever.services.chat.ChatService import net.psforever.services.chat.ChatService
import net.psforever.services.chat.ChatService.ChatChannel import net.psforever.services.chat.ChatService.ChatChannel
import scala.concurrent.ExecutionContextExecutor import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration._ import scala.concurrent.duration._
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.services.{CavernRotationService, InterstellarClusterService}
import net.psforever.types.ChatMessageType.UNK_229
object ChatActor { object ChatActor {
def apply( def apply(
@ -44,6 +47,63 @@ object ChatActor {
private case class ListingResponse(listing: Receptionist.Listing) extends Command private case class ListingResponse(listing: Receptionist.Listing) extends Command
private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
/**
* For a provided number of facility nanite transfer unit resource silos,
* charge the facility's silo with an expected amount of nanite transfer units.
* @see `Amenity`
* @see `ChatMsg`
* @see `ResourceSilo`
* @see `ResourceSilo.UpdateChargeLevel`
* @see `SessionActor.Command`
* @see `SessionActor.SendResponse`
* @param session messaging reference back tothe target session
* @param resources the optional number of resources to set to each silo;
* different values provide different resources as indicated below;
* an undefined value also has a condition
* @param silos where to deposit the resources
* @param debugContent something for log output context
*/
private def setBaseResources(
session: ActorRef[SessionActor.Command],
resources: Option[Int],
silos: Iterable[Amenity],
debugContent: String
): Unit = {
if (silos.isEmpty) {
session ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $debugContent", None)
)
}
resources match {
// x = n0% of maximum capacitance
case Some(value) if value > -1 && value < 11 =>
silos.collect {
case silo: ResourceSilo =>
silo.Actor ! ResourceSilo.UpdateChargeLevel(
value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
)
}
// capacitance set to x (where x > 10) exactly, within limits
case Some(value) =>
silos.collect {
case silo: ResourceSilo =>
silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
}
case None =>
// x >= n0% of maximum capacitance and x <= maximum capacitance
val rand = new scala.util.Random
silos.collect {
case silo: ResourceSilo =>
val a = 7
val b = 10 - a
val tenth = silo.MaxNtuCapacitor * 0.1f
silo.Actor ! ResourceSilo.UpdateChargeLevel(
a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
)
}
}
}
} }
class ChatActor( class ChatActor(
@ -57,14 +117,15 @@ class ChatActor(
implicit val ec: ExecutionContextExecutor = context.executionContext implicit val ec: ExecutionContextExecutor = context.executionContext
private[this] val log = org.log4s.getLogger private[this] val log = org.log4s.getLogger
var channels: List[ChatChannel] = List() var channels: List[ChatChannel] = List()
var session: Option[Session] = None var session: Option[Session] = None
var chatService: Option[ActorRef[ChatService.Command]] = None var chatService: Option[ActorRef[ChatService.Command]] = None
var silenceTimer: Cancellable = Default.Cancellable var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
var silenceTimer: Cancellable = Default.Cancellable
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] { val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
case ChatService.MessageResponse(session, message, channel) => IncomingMessage(session, message, channel) case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
} }
context.system.receptionist ! Receptionist.Find( context.system.receptionist ! Receptionist.Find(
@ -72,43 +133,63 @@ class ChatActor(
context.messageAdapter[Receptionist.Listing](ListingResponse) context.messageAdapter[Receptionist.Listing](ListingResponse)
) )
context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.messageAdapter[Receptionist.Listing](ListingResponse)
)
def start(): Behavior[Command] = { def start(): Behavior[Command] = {
Behaviors Behaviors
.receiveMessage[Command] { .receiveMessage[Command] {
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) => case ListingResponse(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
chatService = Some(listings.head) listings.headOption match {
channels ++= List(ChatChannel.Default()) case Some(ref) =>
postStartBehaviour() cluster = Some(ref)
postStartBehaviour()
case None =>
context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.messageAdapter[Receptionist.Listing](ListingResponse)
)
Behaviors.same
}
case SetSession(newSession) => case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
session = Some(newSession) chatService = Some(listings.head)
postStartBehaviour() channels ++= List(ChatChannel.Default())
postStartBehaviour()
case other => case SetSession(newSession) =>
buffer.stash(other) session = Some(newSession)
Behaviors.same postStartBehaviour()
}
case other =>
buffer.stash(other)
Behaviors.same
}
} }
def postStartBehaviour(): Behavior[Command] = { def postStartBehaviour(): Behavior[Command] = {
(session, chatService) match { (session, chatService, cluster) match {
case (Some(session), Some(chatService)) if session.player != null => case (Some(_session), Some(_chatService), Some(_cluster)) if _session.player != null =>
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, ChatChannel.Default()) _chatService ! ChatService.JoinChannel(chatServiceAdapter, _session, ChatChannel.Default())
buffer.unstashAll(active(session, chatService)) buffer.unstashAll(active(_session, _chatService, _cluster))
case _ => case _ =>
Behaviors.same Behaviors.same
} }
} }
def active(session: Session, chatService: ActorRef[ChatService.Command]): Behavior[Command] = { def active(
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command]
): Behavior[Command] = {
import ChatMessageType._ import ChatMessageType._
Behaviors Behaviors
.receiveMessagePartial[Command] { .receiveMessagePartial[Command] {
case SetSession(newSession) => case SetSession(newSession) =>
active(newSession, chatService) active(newSession, chatService,cluster)
case JoinChannel(channel) => case JoinChannel(channel) =>
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel) chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
@ -275,10 +356,63 @@ class ChatActor(
} }
sessionActor ! SessionActor.SendResponse(message) sessionActor ! SessionActor.SendResponse(message)
case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+")
val customNtuValue = buffer.lift(1) match {
case Some(x) if x.toIntOption.nonEmpty => Some(x.toInt)
case _ => None
}
val silos = {
val position = session.player.Position
session.zone.Buildings.values
.filter { building =>
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position, position) < soi2
}
}
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent="")
case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+")
val (zoneOpt, lockVal) = (buffer.lift(1), buffer.lift(2)) match {
case (Some(x), Some(y)) =>
val zone = if (x.toIntOption.nonEmpty) {
val xInt = x.toInt
Zones.zones.find(_.Number == xInt)
} else {
Zones.zones.find(z => z.id.equals(x))
}
val value = if (y.toIntOption.nonEmpty && y.toInt == 0) {
0
} else {
1
}
(zone, Some(value))
case _ =>
(None, None)
}
(zoneOpt, lockVal) match {
case (Some(zone), Some(lock)) if zone.id.startsWith("c") =>
//caverns must be rotated in an order
if (lock == 0) {
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneUnlock(zone.id))
} else {
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneLock(zone.id))
}
case (Some(zone), Some(lock)) =>
//normal zones can lock when all facilities and towers on it belong to the same faction
//normal zones can lock when ???
case _ => ;
}
case (U_CMT_ZONEROTATE, _, contents) if gmCommandAllowed =>
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
/** Messages starting with ! are custom chat commands */ /** Messages starting with ! are custom chat commands */
case (messageType, recipient, contents) if contents.startsWith("!") => case (messageType, recipient, contents) if contents.startsWith("!") =>
(messageType, recipient, contents) match { (messageType, recipient, contents) match {
case (_, _, contents) if contents.startsWith("!whitetext ") && session.account.gm => case (_, _, _contents) if _contents.startsWith("!whitetext ") && session.account.gm =>
chatService ! ChatService.Message( chatService ! ChatService.Message(
session, session,
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None), ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
@ -293,8 +427,8 @@ class ChatActor(
log.info(loc) log.info(loc)
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc)) sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
case (_, _, contents) if contents.startsWith("!list") => case (_, _, content) if content.startsWith("!list") =>
val zone = contents.split(" ").lift(1) match { val zone = content.split(" ").lift(1) match {
case None => case None =>
Some(session.zone) Some(session.zone)
case Some(id) => case Some(id) =>
@ -302,7 +436,7 @@ class ChatActor(
} }
zone match { zone match {
case Some(zone) => case Some(inZone) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg( ChatMsg(
CMT_GMOPEN, CMT_GMOPEN,
@ -313,7 +447,7 @@ class ChatActor(
) )
) )
(zone.LivePlayers ++ zone.Corpses) (inZone.LivePlayers ++ inZone.Corpses)
.filter(_.CharId != session.player.CharId) .filter(_.CharId != session.player.CharId)
.sortBy(p => (p.Name, !p.isAlive)) .sortBy(p => (p.Name, !p.isAlive))
.foreach(player => { .foreach(player => {
@ -323,7 +457,7 @@ class ChatActor(
CMT_GMOPEN, CMT_GMOPEN,
message.wideContents, message.wideContents,
"Server", "Server",
s"${color}${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}", s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
message.note message.note
) )
) )
@ -340,8 +474,8 @@ class ChatActor(
) )
} }
case (_, _, contents) if contents.startsWith("!ntu") && gmCommandAllowed => case (_, _, content) if content.startsWith("!ntu") && gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+") val buffer = content.toLowerCase.split("\\s+")
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match { val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt)) case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt)) case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
@ -363,39 +497,16 @@ class ChatActor(
session.zone.Buildings.values session.zone.Buildings.values
}) })
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } } .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
if (silos.isEmpty) { ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent=s"$facility")
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $facility", None) case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
) val buffer = contents.toLowerCase.split("\\s+")
} cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
customNtuValue match { case Some("-list") | Some("-l") =>
// x = n0% of maximum capacitance CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case Some(value) if value > -1 && value < 11 => case _ =>
silos.collect { CavernRotationService.HurryNextRotation
case silo: ResourceSilo => })
silo.Actor ! ResourceSilo.UpdateChargeLevel(
value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
)
}
// capacitance set to x (where x > 10) exactly, within limits
case Some(value) =>
silos.collect {
case silo: ResourceSilo =>
silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
}
case None =>
// x >= n0% of maximum capacitance and x <= maximum capacitance
val rand = new scala.util.Random
silos.collect {
case silo: ResourceSilo =>
val a = 7
val b = 10 - a
val tenth = silo.MaxNtuCapacitor * 0.1f
silo.Actor ! ResourceSilo.UpdateChargeLevel(
a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
)
}
}
case _ => case _ =>
// unknown ! commands are ignored // unknown ! commands are ignored

View file

@ -2,7 +2,7 @@ package net.psforever.actors.session
import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed} import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, OneForOneStrategy, SupervisorStrategy, typed}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.actors.net.MiddlewareActor import net.psforever.actors.net.MiddlewareActor
@ -17,7 +17,8 @@ import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVeh
import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
import net.psforever.objects.equipment._ import net.psforever.objects.equipment._
import net.psforever.objects.guid._ import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.objects.locker.LockerContainer import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
@ -55,6 +56,7 @@ import net.psforever.packet._
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _} import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
import net.psforever.services.ServiceManager.{Lookup, LookupResult} import net.psforever.services.ServiceManager.{Lookup, LookupResult}
import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData} import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
@ -66,7 +68,7 @@ import net.psforever.services.properties.PropertyOverrideManager
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
import net.psforever.services.hart.HartTimer import net.psforever.services.hart.HartTimer
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS} import net.psforever.services.{CavernRotationService, RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS}
import net.psforever.types._ import net.psforever.types._
import net.psforever.util.{Config, DefinitionUtil} import net.psforever.util.{Config, DefinitionUtil}
import net.psforever.zones.Zones import net.psforever.zones.Zones
@ -289,6 +291,118 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
var respawnTimer: Cancellable = Default.Cancellable var respawnTimer: Cancellable = Default.Cancellable
var zoningTimer: Cancellable = Default.Cancellable var zoningTimer: Cancellable = Default.Cancellable
override def supervisorStrategy: SupervisorStrategy = {
import net.psforever.objects.inventory.InventoryDisarrayException
import java.io.{StringWriter, PrintWriter}
OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case ide: InventoryDisarrayException =>
attemptRecoverFromInventoryDisarrayException(ide.inventory)
//re-evaluate results
if (ide.inventory.ElementsOnGridMatchList() > 0) {
val sw = new StringWriter
ide.printStackTrace(new PrintWriter(sw))
log.error(sw.toString)
ImmediateDisconnect()
SupervisorStrategy.stop
} else {
SupervisorStrategy.resume
}
case e =>
val sw = new StringWriter
e.printStackTrace(new PrintWriter(sw))
log.error(sw.toString)
ImmediateDisconnect()
SupervisorStrategy.stop
}
}
def attemptRecoverFromInventoryDisarrayException(inv: GridInventory): Unit = {
inv.ElementsInListCollideInGrid() match {
case Nil => ;
case overlaps =>
val previousItems = inv.Clear()
val allOverlaps = overlaps.flatten.sortBy { entry =>
val tile = entry.obj.Definition.Tile
tile.Width * tile.Height
}.toSet
val notCollidingRemainder = previousItems.filterNot(allOverlaps.contains)
notCollidingRemainder.foreach { entry =>
inv.InsertQuickly(entry.start, entry.obj)
}
var didNotFit : List[Equipment] = Nil
allOverlaps.foreach { entry =>
inv.Fit(entry.obj.Definition.Tile) match {
case Some(newStart) =>
inv.InsertQuickly(newStart, entry.obj)
case None =>
didNotFit = didNotFit :+ entry.obj
}
}
//completely clear the inventory
val pguid = player.GUID
val equipmentInHand = player.Slot(player.DrawnSlot).Equipment
//redraw suit
sendResponse(ArmorChangedMessage(
pguid,
player.ExoSuit,
InfantryLoadout.DetermineSubtypeA(player.ExoSuit, equipmentInHand)
))
//redraw item in free hand (if)
player.FreeHand.Equipment match {
case Some(item) =>
sendResponse(ObjectCreateDetailedMessage(
item.Definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(pguid, Player.FreeHandSlot),
item.Definition.Packet.DetailedConstructorData(item).get
))
case _ => ;
}
//redraw items in holsters
player.Holsters().zipWithIndex.foreach { case (slot, index) =>
slot.Equipment match {
case Some(item) =>
sendResponse(ObjectCreateDetailedMessage(
item.Definition.ObjectId,
item.GUID,
item.Definition.Packet.DetailedConstructorData(item).get
))
case _ => ;
}
}
//redraw raised hand (if)
equipmentInHand match {
case Some(_) =>
sendResponse(ObjectHeldMessage(pguid, player.DrawnSlot, unk1 = true))
case _ => ;
}
//redraw inventory items
val recoveredItems = inv.Items
recoveredItems.foreach { entry =>
val item = entry.obj
sendResponse(ObjectCreateDetailedMessage(
item.Definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(pguid, entry.start),
item.Definition.Packet.DetailedConstructorData(item).get
))
}
//drop items that did not fit
val placementData = PlacementData(player.Position, Vector3.z(player.Orientation.z))
didNotFit.foreach { item =>
sendResponse(ObjectCreateMessage(
item.Definition.ObjectId,
item.GUID,
DroppedItemData(
placementData,
item.Definition.Packet.ConstructorData(item).get
)
))
}
}
}
def session: Session = _session def session: Session = _session
def session_=(session: Session): Unit = { def session_=(session: Session): Unit = {
@ -419,6 +533,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case ICS.InterstellarClusterServiceKey.Listing(listings) => case ICS.InterstellarClusterServiceKey.Listing(listings) =>
cluster = listings.head cluster = listings.head
case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
listings.head ! SendCavernRotationUpdates(self)
// Avatar subscription update // Avatar subscription update
case avatar: Avatar => case avatar: Avatar =>
/* /*
@ -601,6 +718,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case GalaxyResponse.MapUpdate(msg) => case GalaxyResponse.MapUpdate(msg) =>
sendResponse(msg) sendResponse(msg)
case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
val faction = player.Faction
val from = fromFactions.contains(faction)
val to = toFactions.contains(faction)
if (from && !to) {
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
} else if (!from && to) {
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
}
case GalaxyResponse.FlagMapUpdate(msg) => case GalaxyResponse.FlagMapUpdate(msg) =>
sendResponse(msg) sendResponse(msg)
@ -651,6 +778,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//wait patiently //wait patiently
} }
} }
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
val popBO = 0
val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
case GalaxyResponse.SendResponse(msg) =>
sendResponse(msg)
} }
case LocalServiceResponse(toChannel, guid, reply) => case LocalServiceResponse(toChannel, guid, reply) =>
@ -1046,10 +1187,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint)) LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint))
case None => case None =>
log.warn( log.warn(
s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService; sending home" s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService"
) )
//Thread.sleep(1000) // throttle in case of infinite loop if (Config.app.game.warpGates.defaultToSanctuaryDestination) {
RequestSanctuaryZoneSpawn(player, currentZone = 0) log.warn(s"SpawnPointResponse: sending ${player.Name} home")
RequestSanctuaryZoneSpawn(player, currentZone = 0)
}
} }
} }
@ -1104,7 +1247,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//CaptureFlagUpdateMessage() //CaptureFlagUpdateMessage()
//VanuModuleUpdateMessage() //VanuModuleUpdateMessage()
//ModuleLimitsMessage() //ModuleLimitsMessage()
sendResponse(ZoneInfoMessage(continentNumber, true, 0)) val isCavern = continent.map.cavern
sendResponse(ZoneInfoMessage(continentNumber, true, if (isCavern) { Int.MaxValue.toLong } else { 0L }))
sendResponse(ZoneLockInfoMessage(continentNumber, false, true)) sendResponse(ZoneLockInfoMessage(continentNumber, false, true))
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0)) sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
sendResponse( sendResponse(
@ -1117,6 +1261,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
) )
) //normally set for all zones in bulk; should be fine manually updating per zone like this ) //normally set for all zones in bulk; should be fine manually updating per zone like this
} }
ServiceManager.receptionist ! Receptionist.Find(
CavernRotationService.CavernRotationServiceKey,
context.self
)
LivePlayerList.Add(avatar.id, avatar) LivePlayerList.Add(avatar.id, avatar)
//PropertyOverrideMessage //PropertyOverrideMessage
@ -1133,7 +1281,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
//the following subscriptions last until character switch/logout //the following subscriptions last until character switch/logout
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets) squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
player.Zone match { player.Zone match {
@ -2085,15 +2233,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false)) sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false))
//cleanup //cleanup
(old_holsters ++ old_inventory).foreach { (old_holsters ++ old_inventory).foreach {
case (obj, guid) => case (obj, objGuid) =>
sendResponse(ObjectDeleteMessage(guid, 0)) sendResponse(ObjectDeleteMessage(objGuid, 0))
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
} }
//redraw //redraw
if (maxhand) { if (maxhand) {
TaskWorkflow.execute(HoldNewEquipmentUp(player)( TaskWorkflow.execute(HoldNewEquipmentUp(player)(
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
0 slot = 0
)) ))
} }
ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
@ -2147,13 +2295,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu) continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
case Some(carrier: Player) => case Some(carrier: Player) =>
log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}") log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
case None => case _ =>
log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.") log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
} }
case _ => case _ =>
log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid") log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid")
} }
case _ => ; // Nothing to drop, do nothing. case _ => ; // Nothing to drop, do nothing.
} }
} }
@ -5653,7 +5800,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
) => ) =>
CancelZoningProcessWithDescriptiveReason("cancel_use") CancelZoningProcessWithDescriptiveReason("cancel_use")
if (deadState != DeadState.RespawnTime) { if (deadState != DeadState.RespawnTime) {
continent.Buildings.values.find(building => building.GUID == building_guid) match { continent.Buildings.values.find(_.GUID == building_guid) match {
case Some(wg: WarpGate) if wg.Active && (GetKnownVehicleAndSeat() match { case Some(wg: WarpGate) if wg.Active && (GetKnownVehicleAndSeat() match {
case (Some(vehicle), _) => case (Some(vehicle), _) =>
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition) wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
@ -5665,6 +5812,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
destinationZoneGuid.guid, destinationZoneGuid.guid,
player, player,
destinationBuildingGuid, destinationBuildingGuid,
continent.Number,
building_guid,
context.self context.self
) )
log.info(s"${player.Name} wants to use a warp gate") log.info(s"${player.Name} wants to use a warp gate")
@ -5949,7 +6098,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
summary, summary,
desc desc
) => ) =>
log.warn(s"${player.Name} filed a bug report") log.warn(s"${player.Name} filed a bug report - it might be something important")
log.debug(s"$msg") log.debug(s"$msg")
case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
@ -6843,42 +6992,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
def initGate(continentNumber: Int, buildingNumber: Int, building: Building): Unit = { def initGate(continentNumber: Int, buildingNumber: Int, building: Building): Unit = {
building match { building match {
case wg: WarpGate => case wg: WarpGate =>
sendResponse( sendResponse(building.infoUpdateMessage())
BuildingInfoUpdateMessage(
building.Zone.Number,
building.MapId,
ntu_level = 0,
is_hacked = false,
empire_hack = PlanetSideEmpire.NEUTRAL,
hack_time_remaining = 0,
building.Faction,
unk1 = 0,
unk1x = None,
PlanetSideGeneratorState.Normal,
spawn_tubes_normal = true,
force_dome_active = false,
lattice_benefit = 0,
cavern_benefit = 0,
unk4 = Nil,
unk5 = 0,
unk6 = false,
unk7 = 8,
unk7x = None,
boost_spawn_pain = false,
boost_generator_pain = false
)
)
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0))) sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0)))
//TODO one faction knows which gates are broadcast for another faction? if (wg.Broadcast(player.Faction)) {
sendResponse( sendResponse(
BroadcastWarpgateUpdateMessage( BroadcastWarpgateUpdateMessage(
continentNumber, continentNumber,
buildingNumber, buildingNumber,
wg.Broadcast(PlanetSideEmpire.TR), player.Faction
wg.Broadcast(PlanetSideEmpire.NC), )
wg.Broadcast(PlanetSideEmpire.VS)
) )
) }
case _ => ; case _ => ;
} }
} }
@ -7572,6 +7696,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) { if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.") log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary.")) sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
ImmediateDisconnect()
} else { } else {
continent.GUID(player.VehicleSeated) match { continent.GUID(player.VehicleSeated) match {
case Some(obj: Vehicle) if !obj.Destroyed => case Some(obj: Vehicle) if !obj.Destroyed =>
@ -8776,8 +8901,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//for other zones ... //for other zones ...
//biolabs have/grant benefits //biolabs have/grant benefits
val cryoBenefit: Float = toSpawnPoint.Owner match { val cryoBenefit: Float = toSpawnPoint.Owner match {
case b: Building if b.hasLatticeBenefit(GlobalDefinitions.cryo_facility) => 0.5f case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) => 0.5f
case _ => 1f case _ => 1f
} }
//TODO cumulative death penalty //TODO cumulative death penalty
toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds
@ -9129,7 +9254,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
def KeepAlivePersistence(): Unit = { def KeepAlivePersistence(): Unit = {
interimUngunnedVehicle = None interimUngunnedVehicle = None
persist() persist()
turnCounterFunc(player.GUID) if (player.HasGUID) {
turnCounterFunc(player.GUID)
} else {
turnCounterFunc(PlanetSideGUID(0))
}
} }
/** /**
@ -9235,7 +9364,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
val initialQuality = tool.FireMode match { val initialQuality = tool.FireMode match {
case mode: ChargeFireModeDefinition => case mode: ChargeFireModeDefinition =>
ProjectileQuality.Modified( ProjectileQuality.Modified(
projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) / mode.Time.toFloat {
val timeInterval = projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis())
timeInterval.toFloat / mode.Time.toFloat
}
) )
case _ => case _ =>
ProjectileQuality.Normal ProjectileQuality.Normal

View file

@ -1,33 +1,44 @@
package net.psforever.actors.zone package net.psforever.actors.zone
import akka.{actor => classic}
import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.{actor => classic}
import net.psforever.actors.commands.NtuCommand import net.psforever.actors.commands.NtuCommand
import net.psforever.objects.NtuContainer import net.psforever.actors.zone.building._
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.persistence import net.psforever.persistence
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.{InterstellarClusterService, Service, ServiceManager} import net.psforever.services.{InterstellarClusterService, ServiceManager}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState} import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Database._ import net.psforever.util.Database.ctx
import org.log4s.Logger
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
final case class BuildingControlDetails(
galaxyService: classic.ActorRef = null,
interstellarCluster: ActorRef[InterstellarClusterService.Command] = null
)
object BuildingActor { object BuildingActor {
def apply(zone: Zone, building: Building): Behavior[Command] = def apply(zone: Zone, building: Building): Behavior[Command] =
Behaviors Behaviors
.supervise[Command] { .supervise[Command] {
Behaviors.withStash(100) { buffer => Behaviors.withStash(capacity = 100) { buffer =>
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building).start()) val logic: BuildingLogic = building match {
case _: WarpGate =>
WarpGateLogic
case _ if zone.map.cavern =>
CavernFacilityLogic
case _ if building.BuildingType == StructureType.Facility =>
MajorFacilityLogic
case _ =>
FacilityLogic
}
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building, logic).start())
} }
} }
.onFailure[Exception](SupervisorStrategy.restart) .onFailure[Exception](SupervisorStrategy.restart)
@ -40,13 +51,7 @@ object BuildingActor {
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command
final case class UpdateForceDome(state: Option[Boolean]) extends Command final case class AlertToFactionChange(building: Building) extends Command
object UpdateForceDome {
def apply(): UpdateForceDome = UpdateForceDome(None)
def apply(state: Boolean): UpdateForceDome = UpdateForceDome(Some(state))
}
// TODO remove // TODO remove
// Changes to building objects should go through BuildingActor // Changes to building objects should go through BuildingActor
@ -70,55 +75,93 @@ object BuildingActor {
final case class PowerOff() extends Command final case class PowerOff() extends Command
/** /**
* The natural conditions of a facility that is not eligible for its capitol force dome to be expanded. * Set a facility affiliated to one faction to be affiliated to a different faction.
* The only test not employed is whether or not the target building is a capitol. * @param details building and event system references
* Ommission of this condition makes this test capable of evaluating subcapitol eligibility * @param faction faction to which the building is being set
* for capitol force dome expansion. * @param log wrapped-up log for customized debug information
* @param building the target building
* @return `true`, if the conditions for capitol force dome are not met;
* `false`, otherwise
*/ */
def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = { def setFactionTo(
building.Faction == PlanetSideEmpire.NEUTRAL || details: BuildingWrapper,
building.NtuLevel == 0 || faction: PlanetSideEmpire.Value,
(building.Generator match { log: BuildingWrapper => Logger
case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed ): Unit = {
case _ => false setFactionInDatabase(details, faction, log)
}) setFactionOnEntity(details, faction, log)
} }
/** /**
* If this building is a capitol major facility, * Set a facility affiliated to one faction to be affiliated to a different faction.
* use the faction affinity, the generator status, and the resource silo's capacitance level * Handle the database entry updates to reflect the proper faction affiliation.
* to determine if the capitol force dome should be active. * @param details building and event system references
* @param building the building being evaluated * @param faction faction to which the building is being set
* @return the condition of the capitol force dome; * @param log wrapped-up log for customized debug information
* `None`, if the facility is not a capitol building;
* `Some(true|false)` to indicate the state of the force dome
*/ */
def checkForceDomeStatus(building: Building): Option[Boolean] = { def setFactionInDatabase(
if (building.IsCapitol) { details: BuildingWrapper,
val originalStatus = building.ForceDomeActive faction: PlanetSideEmpire.Value,
val faction = building.Faction log: BuildingWrapper => Logger
val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) { ): Unit = {
false val building = details.building
} else { val zone = building.Zone
val ownedSubCapitols = building.Neighbours(faction) match { import ctx._
case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) } import scala.concurrent.ExecutionContext.Implicits.global
case None => 0 ctx
} .run(
if (originalStatus && ownedSubCapitols <= 1) { query[persistence.Building]
false .filter(_.localId == lift(building.MapId))
} else if (!originalStatus && ownedSubCapitols > 1) { .filter(_.zoneId == lift(zone.Number))
true )
} else { .onComplete {
originalStatus case Success(res) =>
} res.headOption match {
case Some(_) =>
ctx
.run(
query[persistence.Building]
.filter(_.localId == lift(building.MapId))
.filter(_.zoneId == lift(zone.Number))
.update(_.factionId -> lift(faction.id))
)
.onComplete {
case Success(_) =>
case Failure(e) => log(details).error(e.getMessage)
}
case _ =>
ctx
.run(
query[persistence.Building]
.insert(
_.localId -> lift(building.MapId),
_.factionId -> lift(faction.id),
_.zoneId -> lift(zone.Number)
)
)
.onComplete {
case Success(_) =>
case Failure(e) => log(details).error(e.getMessage)
}
}
case Failure(e) => log(details).error(e.getMessage)
} }
Some(updatedStatus) }
} else {
None /**
} * Set a facility affiliated to one faction to be affiliated to a different faction.
* Handle the facility entry to reflect the correct faction affiliation.
* @param details building and event system references
* @param faction faction to which the building is being set
* @param log wrapped-up log for customized debug information
*/
def setFactionOnEntity(
details: BuildingWrapper,
faction: PlanetSideEmpire.Value,
log: BuildingWrapper => Logger
): Unit = {
val building = details.building
val zone = building.Zone
building.Faction = faction
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
} }
} }
@ -126,371 +169,78 @@ class BuildingActor(
context: ActorContext[BuildingActor.Command], context: ActorContext[BuildingActor.Command],
buffer: StashBuffer[BuildingActor.Command], buffer: StashBuffer[BuildingActor.Command],
zone: Zone, zone: Zone,
building: Building building: Building,
logic: BuildingLogic
) { ) {
import BuildingActor._ import BuildingActor._
private[this] val log = org.log4s.getLogger
var galaxyService: Option[classic.ActorRef] = None
var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None
var hasNtuSupply: Boolean = true
context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
)
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
"galaxy",
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
)
def start(): Behavior[Command] = { def start(): Behavior[Command] = {
context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
)
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
"galaxy",
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
)
setup(BuildingControlDetails())
}
def setup(details: BuildingControlDetails): Behavior[Command] = {
Behaviors.receiveMessage { Behaviors.receiveMessage {
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) => case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
interstellarCluster = listings.headOption switchToBehavior(details.copy(interstellarCluster = listings.head))
postStartBehaviour()
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) => case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
request match { switchToBehavior(request match {
case "galaxy" => galaxyService = Some(endpoint) case "galaxy" => details.copy(galaxyService = endpoint)
} case _ => details
postStartBehaviour() })
case other => case other =>
buffer.stash(other) buffer.stash(other)
Behaviors.same setup(details)
} }
} }
def postStartBehaviour(): Behavior[Command] = { def switchToBehavior(details: BuildingControlDetails): Behavior[Command] = {
(galaxyService, interstellarCluster) match { if (details.galaxyService != null && details.interstellarCluster != null) {
case (Some(_galaxyService), Some(_interstellarCluster)) => buffer.unstashAll(active(logic.wrapper(building, context, details)))
buffer.unstashAll(active(_galaxyService, _interstellarCluster)) } else {
case _ => setup(details)
Behaviors.same
} }
} }
def active( def active(details: BuildingWrapper): Behavior[Command] = {
galaxyService: classic.ActorRef,
interstellarCluster: ActorRef[InterstellarClusterService.Command]
): Behavior[Command] = {
Behaviors.receiveMessagePartial { Behaviors.receiveMessagePartial {
case SetFaction(faction) => case SetFaction(faction) =>
setFactionTo(faction, galaxyService) logic.setFactionTo(details, faction)
case AlertToFactionChange(neighbor) =>
logic.alertToFactionChange(details, neighbor)
Behaviors.same Behaviors.same
case MapUpdate() => case MapUpdate() =>
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage())) details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
Behaviors.same Behaviors.same
case UpdateForceDome(stateOpt) => case AmenityStateChange(amenity, data) =>
stateOpt match { logic.amenityStateChange(details, amenity, data)
case Some(updatedStatus) if building.IsCapitol && updatedStatus != building.ForceDomeActive =>
updateForceDomeStatus(updatedStatus, mapUpdateOnChange = true)
case _ =>
alignForceDomeStatus()
}
Behaviors.same
case AmenityStateChange(gen: Generator, data) =>
if (generatorStateChange(gen, data)) {
// Request all buildings update their map data to refresh lattice linked benefits
zone.actor ! ZoneActor.ZoneMapUpdate()
}
Behaviors.same
case AmenityStateChange(terminal: CaptureTerminal, data) =>
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log.warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required
Behaviors.same
case AmenityStateChange(_, _) =>
//TODO when parameter object is finally immutable, perform analysis on it to determine specific actions
//for now, just update the map
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
Behaviors.same
case PowerOff() => case PowerOff() =>
building.Generator match { logic.powerOff(details)
case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted()
case _ => powerLost()
}
Behaviors.same
case PowerOn() => case PowerOn() =>
building.Generator match { logic.powerOn(details)
case Some(gen) if building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu()
case _ => powerRestored()
}
Behaviors.same
case msg @ NtuDepleted() => case NtuDepleted() =>
// Someone let the base run out of nanites. No one gets anything. logic.ntuDepleted(details)
building.Amenities.foreach { amenity =>
amenity.Actor ! msg
}
setFactionTo(PlanetSideEmpire.NEUTRAL, galaxyService)
hasNtuSupply = false
Behaviors.same
case msg @ SuppliedWithNtu() => case SuppliedWithNtu() =>
// Auto-repair restart, mainly. If the Generator works, power should be restored too. logic.suppliedWithNtu(details)
hasNtuSupply = true
building.Amenities.foreach { amenity =>
amenity.Actor ! msg
}
Behaviors.same
case Ntu(msg) => case Ntu(msg) =>
ntu(msg) logic.ntu(details, msg)
}
}
def generatorStateChange(generator: Generator, event: Any): Boolean = {
event match {
case Some(GeneratorControl.Event.UnderAttack) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 15)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
false
case Some(GeneratorControl.Event.Critical) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.PlanetsideAttributeToAll(guid, 46, 1)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
true
case Some(GeneratorControl.Event.Destabilized) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 16)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
false
case Some(GeneratorControl.Event.Destroyed) =>
true
case Some(GeneratorControl.Event.Offline) =>
powerLost()
alignForceDomeStatus(mapUpdateOnChange = false)
val zone = building.Zone
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
building.PlayersInSOI.foreach { player =>
zone.AvatarEvents ! AvatarServiceMessage(player.Name, msg)
} //???
true
case Some(GeneratorControl.Event.Normal) =>
true
case Some(GeneratorControl.Event.Online) =>
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
powerRestored()
alignForceDomeStatus(mapUpdateOnChange = false)
val events = zone.AvatarEvents
val guid = building.GUID
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
val msg2 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 17)
building.PlayersInSOI.foreach { player =>
val name = player.Name
events ! AvatarServiceMessage(name, msg1) //reset ???; might be global?
events ! AvatarServiceMessage(name, msg2) //This facility's generator is back on line
}
true
case _ =>
false
}
}
def setFactionTo(faction: PlanetSideEmpire.Value, galaxy: classic.ActorRef): Unit = {
if (hasNtuSupply) {
import ctx._
ctx
.run(
query[persistence.Building]
.filter(_.localId == lift(building.MapId))
.filter(_.zoneId == lift(zone.Number))
)
.onComplete {
case Success(res) =>
res.headOption match {
case Some(_) =>
ctx
.run(
query[persistence.Building]
.filter(_.localId == lift(building.MapId))
.filter(_.zoneId == lift(zone.Number))
.update(_.factionId -> lift(building.Faction.id))
)
.onComplete {
case Success(_) =>
case Failure(e) => log.error(e.getMessage)
}
case _ =>
ctx
.run(
query[persistence.Building]
.insert(
_.localId -> lift(building.MapId),
_.factionId -> lift(building.Faction.id),
_.zoneId -> lift(zone.Number)
)
)
.onComplete {
case Success(_) =>
case Failure(e) => log.error(e.getMessage)
}
}
case Failure(e) => log.error(e.getMessage)
}
building.Faction = faction
alignForceDomeStatus(mapUpdateOnChange = false)
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
}
}
/**
* Evaluate the conditions of the building
* and determine if its capitol force dome state should be updated
* to reflect the actual conditions of the base or its surrounding bases.
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
* and has the capitol force dome has a dependency upon it,
* pass a message onto that facility that it should check its own state alignment.
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
*/
def alignForceDomeStatus(mapUpdateOnChange: Boolean = true): Unit = {
BuildingActor.checkForceDomeStatus(building) match {
case Some(updatedStatus) if updatedStatus != building.ForceDomeActive =>
updateForceDomeStatus(updatedStatus, mapUpdateOnChange)
case None if building.IsSubCapitol =>
building.Neighbours match {
case Some(buildings: Set[Building]) =>
buildings
.filter { _.IsCapitol }
.foreach { _.Actor ! BuildingActor.UpdateForceDome() }
case None => ;
}
case _ => ; //building is neither a capitol nor a subcapitol
}
}
/**
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
* @param updatedStatus the new capitol force dome status
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
*/
def updateForceDomeStatus(updatedStatus: Boolean, mapUpdateOnChange: Boolean): Unit = {
building.ForceDomeActive = updatedStatus
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus)
)
if (mapUpdateOnChange) {
context.self ! BuildingActor.MapUpdate()
}
}
/**
* Power has been severed.
* All installed amenities are distributed a `PowerOff` message
* and are instructed to display their "unpowered" model.
* Additionally, the facility is now rendered unspawnable regardless of its player spawning amenities.
*/
def powerLost(): Unit = {
val zone = building.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val guid = building.GUID
val powerMsg = BuildingActor.PowerOff()
building.Amenities.foreach { amenity =>
amenity.Actor ! powerMsg
}
//amenities disabled; red warning lights
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 1))
//disable spawn target on deployment map
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 0))
}
/**
* Power has been restored.
* All installed amenities are distributed a `PowerOn` message
* and are instructed to display their "powered" model.
* Additionally, the facility is now rendered spawnable if its player spawning amenities are online.
*/
def powerRestored(): Unit = {
val zone = building.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val guid = building.GUID
val powerMsg = BuildingActor.PowerOn()
building.Amenities.foreach { amenity =>
amenity.Actor ! powerMsg
}
//amenities enabled; normal lights
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 0))
//enable spawn target on deployment map
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 1))
}
def ntu(msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
msg match {
case Offer(_, _) =>
Behaviors.same
case Request(amount, replyTo) =>
building match {
case b: WarpGate =>
//warp gates are an infinite source of nanites
replyTo ! Grant(b, if (b.Active) amount else 0)
Behaviors.same
case _ if building.BuildingType == StructureType.Tower || building.Zone.map.cavern =>
//towers and cavern stuff get free repairs
replyTo ! NtuCommand.Grant(new FakeNtuSource(building), amount)
Behaviors.same
case _ =>
//all other facilities require a storage silo for ntu
building.NtuSource match {
case Some(ntuContainer) =>
ntuContainer.Actor ! msg //needs to redirect
Behaviors.same
case None =>
replyTo ! NtuCommand.Grant(null, 0)
Behaviors.unhandled
}
}
case _ =>
Behaviors.same
} }
} }
} }
class FakeNtuSource(private val building: Building)
extends PlanetSideServerObject
with NtuContainer {
override def NtuCapacitor = Int.MaxValue.toFloat
override def NtuCapacitor_=(a: Float) = Int.MaxValue.toFloat
override def MaxNtuCapacitor = Int.MaxValue.toFloat
override def Faction = building.Faction
override def Zone = building.Zone
override def Definition = null
}

View file

@ -5,7 +5,7 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup} import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, Player, Vehicle} import net.psforever.objects.{ConstructionItem, Player, Vehicle}
@ -13,6 +13,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.util.Database._ import net.psforever.util.Database._
import net.psforever.persistence import net.psforever.persistence
@ -85,22 +86,19 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete { ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete {
case Success(buildings) => case Success(buildings) =>
var capitol: Option[Building] = None
buildings.foreach { building => buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match { zone.BuildingByMapId(building.localId) match {
case Some(_: WarpGate) => ;
//warp gates are controlled by game logic and are better off not restored via the database
case Some(b) => case Some(b) =>
b.Faction = PlanetSideEmpire(building.factionId) if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
if(b.IsCapitol) { b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
capitol = Some(b) b.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(b) }
} }
case None => case None => ;
// TODO this happens during testing, need a way to not always persist during tests // TODO this happens during testing, need a way to not always persist during tests
} }
} }
capitol match {
case Some(b) => b.ForceDomeActive = BuildingActor.checkForceDomeStatus(b).getOrElse(false)
case None => ;
}
case Failure(e) => log.error(e.getMessage) case Failure(e) => log.error(e.getMessage)
} }
@ -121,7 +119,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case PickupItem(guid) => case PickupItem(guid) =>
zone.Ground ! Zone.Ground.PickupItem(guid) zone.Ground ! Zone.Ground.PickupItem(guid)
case BuildDeployable(obj, tool) => case BuildDeployable(obj, _) =>
zone.Deployables ! Zone.Deployable.Build(obj) zone.Deployables ! Zone.Deployable.Build(obj)
case DismissDeployable(obj) => case DismissDeployable(obj) =>

View file

@ -0,0 +1,23 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.{actor => classic}
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.ActorContext
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.structures.Building
import net.psforever.services.InterstellarClusterService
/**
* A package class that conveys the important information for handling facility updates.
* @param building building entity
* @param context message-passing reference
* @param galaxyService event system for state updates to the whole server
* @param interstellarCluster event system for behavior updates from the whole server
*/
final case class BasicBuildingWrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
galaxyService: classic.ActorRef,
interstellarCluster: ActorRef[InterstellarClusterService.Command]
) extends BuildingWrapper

View file

@ -0,0 +1,113 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.ActorContext
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.types.PlanetSideEmpire
import org.log4s.Logger
/**
* Logic that dictates what happens to a particular type of building
* when it receives certain messages on its governing control.
* Try not to transform this into instance classes.
*/
trait BuildingLogic {
import BuildingActor.Command
/**
* Produce a log that borrows from the building name.
* @param details package class that conveys the important information
* @return the custom log
*/
protected def log(details: BuildingWrapper): Logger = {
org.log4s.getLogger(details.building.Name)
}
/**
* Update the status of the relationship between a component installed in a facility
* and the facility's status itself.
* @param details package class that conveys the important information
* @param entity the installed `Amenity` entity
* @param data optional information
* @return the next behavior for this control agency messaging system
*/
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command]
/**
* The facility has lost power.
* Update all related subsystems and statuses.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def powerOff(details: BuildingWrapper): Behavior[Command]
/**
* The facility has regained power.
* Update all related subsystems and statuses.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def powerOn(details: BuildingWrapper): Behavior[Command]
/**
* The facility has run out of nanite resources.
* Update all related subsystems and statuses.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def ntuDepleted(details: BuildingWrapper): Behavior[Command]
/**
* The facility has had its nanite resources restored, even if partially.
* Update all related subsystems and statuses.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def suppliedWithNtu(details: BuildingWrapper): Behavior[Command]
/**
* The facility will change its faction affiliation.
* Update all related subsystems and statuses.
* @param details package class that conveys the important information
* @param faction the faction affiliation to which the facility will update
* @return the next behavior for this control agency messaging system
*/
def setFactionTo(details: BuildingWrapper, faction: PlanetSideEmpire.Value): Behavior[Command]
/**
* A facility that influences this facility has changed its faction affiliation.
* Update all related subsystems and statuses of this facility.
* @param details package class that conveys the important information
* @param building the neighbor facility that has had its faction changed
* @return the next behavior for this control agency messaging system
*/
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command]
/**
* The facility has had its nanite resources changed in some way.
* Update all related subsystems and statuses of this facility.
* @see `NtuCommand.Command`
* @param details package class that conveys the important information
* @param msg the original message that instigated this upoate
* @return the next behavior for this control agency messaging system
*/
def ntu(details: BuildingWrapper, msg: NtuCommand.Command): Behavior[Command]
/**
* Produce an appropriate representation of the facility for the given logic implementation.
* @param building building entity
* @param context message-passing reference
* @param details temporary storage to retain still-allocating reousces during facility startup
* @return the representation of the building and assorted connecting and reporting outlets
*/
def wrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
details: BuildingControlDetails
): BuildingWrapper = {
BasicBuildingWrapper(building, context, details.galaxyService, details.interstellarCluster)
}
}

View file

@ -0,0 +1,28 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.{actor => classic}
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.ActorContext
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.structures.Building
import net.psforever.services.InterstellarClusterService
/**
* A package class that conveys the important information for handling facility updates.
* @see `BuildingActor`
* @see `BuildingLogic`
* @see `BuildingWrapper`
* @see `GalaxyService`
* @see `InterstellarClusterService`
*/
trait BuildingWrapper {
/** building entity */
def building: Building
/** message-passing reference */
def context: ActorContext[BuildingActor.Command]
/** event system for state updates to the whole server */
def galaxyService: classic.ActorRef
/** event system for behavior updates from the whole server */
def interstellarCluster: ActorRef[InterstellarClusterService.Command]
}

View file

@ -0,0 +1,108 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
/**
* The logic that governs facilities and structures found in the cavern regions.
*/
case object CavernFacilityLogic
extends BuildingLogic {
import BuildingActor.Command
override def wrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
details: BuildingControlDetails
): BuildingWrapper = {
FacilityWrapper(building, context, details.galaxyService, details.interstellarCluster)
}
/**
* Although cavern facilities don't possess many amenities that can be abused by faction enemies
* or need to be statused on the continental map,
* the facilities can be captured and controlled by a particular empire.
* @param details package class that conveys the important information
* @param entity the installed `Amenity` entity
* @param data optional information
* @return the next behavior for this control agency messaging system
*/
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
entity match {
case terminal: CaptureTerminal =>
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}
Behaviors.same
}
def powerOff(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def powerOn(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def ntuDepleted(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def suppliedWithNtu(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def setFactionTo(
details: BuildingWrapper,
faction: PlanetSideEmpire.Value
): Behavior[Command] = {
BuildingActor.setFactionTo(details, faction, log)
val building = details.building
building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) }
Behaviors.same
}
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
Behaviors.same
}
/**
* Cavern facilities get free auto-repair and give out free nanites.
* Do they even care about nanites storage down there?
* @param details package class that conveys the important information
* @param msg the original message that instigated this upoate
* @return the next behavior for this control agency messaging system
*/
def ntu(details: BuildingWrapper, msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
msg match {
case Request(amount, replyTo) =>
replyTo ! NtuCommand.Grant(details.asInstanceOf[FacilityWrapper].supplier, amount)
case _ => ;
}
Behaviors.same
}
}

View file

@ -0,0 +1,103 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
/**
* The logic that governs standard facilities and structures.
*/
case object FacilityLogic
extends BuildingLogic {
import BuildingActor.Command
override def wrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
details: BuildingControlDetails
): BuildingWrapper = {
FacilityWrapper(building, context, details.galaxyService, details.interstellarCluster)
}
/**
* Although mundane facilities don't possess many amenities need to be statused on the continental map,
* the facilities can be captured and controlled by a particular empire
* and many amenities that can be abused by faction enemies.
* @param details package class that conveys the important information
* @param entity the installed `Amenity` entity
* @param data optional information
* @return the next behavior for this control agency messaging system
*/
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
entity match {
case terminal: CaptureTerminal =>
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}
Behaviors.same
}
def powerOff(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def powerOn(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def ntuDepleted(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def suppliedWithNtu(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def setFactionTo(details: BuildingWrapper, faction : PlanetSideEmpire.Value): Behavior[Command] = {
BuildingActor.setFactionTo(details, faction, log)
Behaviors.same
}
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
Behaviors.same
}
/**
* Field towers and other structures that are considered off the grid get free auto-repairs and give out free nanites.
* @param details package class that conveys the important information
* @param msg the original message that instigated this upoate
* @return the next behavior for this control agency messaging system
*/
def ntu(details: BuildingWrapper, msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
msg match {
case Request(amount, replyTo) =>
//towers and stuff stuff get free repairs
replyTo ! NtuCommand.Grant(details.asInstanceOf[FacilityWrapper].supplier, amount)
case _ =>
}
Behaviors.same
}
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.{actor => classic}
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.ActorContext
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.structures.Building
import net.psforever.services.InterstellarClusterService
/**
* A package class that conveys the important information for handling facility updates.
* These sorts of smaller facilities have power systems that are similar to major facilities
* but they lack the installed components to support such functionality.
* A free-floating unlimited power source is provided.
* @see `FacilityLogic`
* @see `FakeNtuSource`
* @param building building entity
* @param context message-passing reference
* @param galaxyService event system for state updates to the whole server
* @param interstellarCluster event system for behavior updates from the whole server
*/
final case class FacilityWrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
galaxyService: classic.ActorRef,
interstellarCluster: ActorRef[InterstellarClusterService.Command]
)
extends BuildingWrapper {
/** a custom source for nanite transfer units */
val supplier = new FakeNtuSource(building)
}

View file

@ -0,0 +1,23 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import net.psforever.objects.{GlobalDefinitions, NtuContainer}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Building
/**
* A nanite transfer unit provision device for this building.
* It does not actually belong to the building as an `Amenity`-level feature.
* In essence, "it does not exist".
* @param building the building
*/
protected class FakeNtuSource(private val building: Building)
extends PlanetSideServerObject
with NtuContainer {
override def NtuCapacitor = Int.MaxValue.toFloat
override def NtuCapacitor_=(a: Float) = Int.MaxValue.toFloat
override def MaxNtuCapacitor = Int.MaxValue.toFloat
override def Faction = building.Faction
override def Zone = building.Zone
override def Definition = GlobalDefinitions.resource_silo
}

View file

@ -0,0 +1,407 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.{actor => classic}
import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState}
/**
* A package class that conveys the important information for handling facility updates.
* Major facilities have power systems and structural components that manage this flow of power.
* The primary concern is a quick means of detecting whether or not the system is operating
* due to a provision of nanites (synchronization on it).
* @see `FacilityLogic`
* @see `Generator`
* @see `ResourceSilo`
* @param building building entity
* @param context message-passing reference
* @param galaxyService event system for state updates to the whole server
* @param interstellarCluster event system for behavior updates from the whole server
*/
final case class MajorFacilityWrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
galaxyService: classic.ActorRef,
interstellarCluster: ActorRef[InterstellarClusterService.Command]
)
extends BuildingWrapper {
var hasNtuSupply: Boolean = true
}
/**
* The logic that governs "major facilities" in the overworld -
* those bases that have lattice connectivity and individual nanite resource stockpiles.
*/
case object MajorFacilityLogic
extends BuildingLogic {
import BuildingActor.Command
override def wrapper(
building: Building,
context: ActorContext[BuildingActor.Command],
details: BuildingControlDetails
): BuildingWrapper = {
MajorFacilityWrapper(building, context, details.galaxyService, details.interstellarCluster)
}
/**
* Evaluate the conditions of the building
* and determine if its capitol force dome state should be updated
* to reflect the actual conditions of the base or its surrounding bases.
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
* and has the capitol force dome has a dependency upon it,
* pass a message onto that facility that it should check its own state alignment.
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
*/
private def alignForceDomeStatus(details: BuildingWrapper, mapUpdateOnChange: Boolean = true): Behavior[Command] = {
val building = details.building
checkForceDomeStatus(building) match {
case Some(updatedStatus) if updatedStatus != building.ForceDomeActive =>
updateForceDomeStatus(details, updatedStatus, mapUpdateOnChange)
case _ => ;
}
Behaviors.same
}
/**
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
* @param updatedStatus the new capitol force dome status
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
*/
private def updateForceDomeStatus(
details: BuildingWrapper,
updatedStatus: Boolean,
mapUpdateOnChange: Boolean
): Unit = {
val building = details.building
val zone = building.Zone
building.ForceDomeActive = updatedStatus
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus)
)
if (mapUpdateOnChange) {
details.context.self ! BuildingActor.MapUpdate()
}
}
/**
* The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
* The only test not employed is whether or not the target building is a capitol.
* Ommission of this condition makes this test capable of evaluating subcapitol eligibility
* for capitol force dome expansion.
* @param building the target building
* @return `true`, if the conditions for capitol force dome are not met;
* `false`, otherwise
*/
private def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
building.Faction == PlanetSideEmpire.NEUTRAL ||
building.NtuLevel == 0 ||
(building.Generator match {
case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed
case _ => false
})
}
/**
* If this building is a capitol major facility,
* use the faction affinity, the generator status, and the resource silo's capacitance level
* to determine if the capitol force dome should be active.
* @param building the building being evaluated
* @return the condition of the capitol force dome;
* `None`, if the facility is not a capitol building;
* `Some(true|false)` to indicate the state of the force dome
*/
def checkForceDomeStatus(building: Building): Option[Boolean] = {
if (building.IsCapitol) {
val originalStatus = building.ForceDomeActive
val faction = building.Faction
val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) {
false
} else {
val ownedSubCapitols = building.Neighbours(faction) match {
case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) }
case None => 0
}
if (originalStatus && ownedSubCapitols <= 1) {
false
} else if (!originalStatus && ownedSubCapitols > 1) {
true
} else {
originalStatus
}
}
Some(updatedStatus)
} else {
None
}
}
/**
* The power structure of major facilities has to be statused on the continental map
* via the state of its nanite-to-energy generator, and
* those facilities can be captured and controlled by a particular empire.
* @param details package class that conveys the important information
* @param entity the installed `Amenity` entity
* @param data optional information
* @return the next behavior for this control agency messaging system
*/
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
entity match {
case gen: Generator =>
if (generatorStateChange(details, gen, data)) {
// Request all buildings update their map data to refresh lattice linked benefits
details.building.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
case terminal: CaptureTerminal =>
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}
Behaviors.same
}
/**
* Power has been severed.
* All installed amenities are distributed a `PowerOff` message
* and are instructed to display their "unpowered" model.
* Additionally, the facility is now rendered unspawnable regardless of its player spawning amenities.
*/
def powerOff(details: BuildingWrapper): Behavior[Command] = {
details.building.Generator match {
case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted()
case _ => powerLost(details)
}
Behaviors.same
}
/**
* Power has been restored.
* All installed amenities are distributed a `PowerOn` message
* and are instructed to display their "powered" model.
* Additionally, the facility is now rendered spawnable if its player spawning amenities are online.
*/
def powerOn(details: BuildingWrapper): Behavior[Command] = {
details.building.Generator match {
case Some(gen) if details.building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu()
case _ => powerRestored(details)
}
Behaviors.same
}
/**
* Running out of nanites is a huge deal.
* Without a supply of nanites, not only does the power go out but
* the faction affiliation of the facility is wiped away and it is rendered neutral.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def ntuDepleted(details: BuildingWrapper): Behavior[Command] = {
// Someone let the base run out of nanites. No one gets anything.
details.building.Amenities.foreach { amenity =>
amenity.Actor ! BuildingActor.NtuDepleted()
}
setFactionTo(details, PlanetSideEmpire.NEUTRAL)
details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = false
Behaviors.same
}
/**
* Running out of nanites is a huge deal.
* Once a supply of nanites has been provided, however,
* the power may be restored if the facility generator is operational.
* @param details package class that conveys the important information
* @return the next behavior for this control agency messaging system
*/
def suppliedWithNtu(details: BuildingWrapper): Behavior[Command] = {
// Auto-repair restart, mainly. If the Generator works, power should be restored too.
details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = true
details.building.Amenities.foreach { amenity =>
amenity.Actor ! BuildingActor.SuppliedWithNtu()
}
Behaviors.same
}
/**
* The generator is an extrememly important amenity of a major facility
* that is given its own status indicators that are apparent from the continental map
* and warning messages that are displayed to everyone who might have an interest in the that particular generator.
* @param details package class that conveys the important information
* @param generator the facility generator
* @param event how the generator changed
* @return `true`, to update the continental map;
* `false`, otherwise
*/
private def generatorStateChange(details: BuildingWrapper, generator: Generator, event: Any): Boolean = {
val building = details.building
val zone = building.Zone
event match {
case Some(GeneratorControl.Event.UnderAttack) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 15)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
false
case Some(GeneratorControl.Event.Critical) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.PlanetsideAttributeToAll(guid, 46, 1)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
true
case Some(GeneratorControl.Event.Destabilized) =>
val events = zone.AvatarEvents
val guid = building.GUID
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 16)
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
false
case Some(GeneratorControl.Event.Destroyed) =>
true
case Some(GeneratorControl.Event.Offline) =>
powerLost(details)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val zone = building.Zone
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
building.PlayersInSOI.foreach { player =>
zone.AvatarEvents ! AvatarServiceMessage(player.Name, msg)
} //???
true
case Some(GeneratorControl.Event.Normal) =>
true
case Some(GeneratorControl.Event.Online) =>
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
powerRestored(details)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val events = zone.AvatarEvents
val guid = building.GUID
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
val msg2 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 17)
building.PlayersInSOI.foreach { player =>
val name = player.Name
events ! AvatarServiceMessage(name, msg1) //reset ???; might be global?
events ! AvatarServiceMessage(name, msg2) //This facility's generator is back on line
}
true
case _ =>
false
}
}
def setFactionTo(
details: BuildingWrapper,
faction: PlanetSideEmpire.Value
): Behavior[Command] = {
if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) {
BuildingActor.setFactionTo(details, faction, log)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val building = details.building
building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) }
}
Behaviors.same
}
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
alignForceDomeStatus(details)
Behaviors.same
}
/**
* Power has been severed.
* All installed amenities are distributed a `PowerOff` message
* and are instructed to display their "unpowered" model.
* Additionally, the facility is now rendered unspawnable regardless of its player spawning amenities.
*/
private def powerLost(details: BuildingWrapper): Behavior[Command] = {
val building = details.building
val zone = building.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val guid = building.GUID
val powerMsg = BuildingActor.PowerOff()
building.Amenities.foreach { amenity =>
amenity.Actor ! powerMsg
}
//amenities disabled; red warning lights
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 1))
//disable spawn target on deployment map
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 0))
Behaviors.same
}
/**
* Power has been restored.
* All installed amenities are distributed a `PowerOn` message
* and are instructed to display their "powered" model.
* Additionally, the facility is now rendered spawnable if its player spawning amenities are online.
*/
private def powerRestored(details: BuildingWrapper): Behavior[Command] = {
val building = details.building
val zone = building.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val guid = building.GUID
val powerMsg = BuildingActor.PowerOn()
building.Amenities.foreach { amenity =>
amenity.Actor ! powerMsg
}
//amenities enabled; normal lights
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 0))
//enable spawn target on deployment map
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 1))
Behaviors.same
}
/**
* Major facilities have individual nanite reservoirs that are depleted
* as other installed components require.
* The main internal use of nanite resources is for auto-repair
* but various nefarious implements can be used to drain nanite resources from the facility directly.
* @param details package class that conveys the important information
* @param msg the original message that instigated this upoate
* @return the next behavior for this control agency messaging system
*/
def ntu(details: BuildingWrapper, msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
msg match {
case Request(_, replyTo) =>
details.building.NtuSource match {
case Some(ntuContainer) =>
ntuContainer.Actor ! msg //redirect
Behaviors.same
case None =>
replyTo ! NtuCommand.Grant(null, 0) //hm ...
}
case _ =>
}
Behaviors.same
}
}

View file

@ -0,0 +1,238 @@
// Copyright (c) 2022 PSForever
package net.psforever.actors.zone.building
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, ZoneActor}
import net.psforever.objects.serverobject.structures.{Amenity, Building, WarpGate}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Config
/**
* The logic that governs warp gates.
*/
case object WarpGateLogic
extends BuildingLogic {
import BuildingActor.Command
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
Behaviors.same
}
def powerOff(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def powerOn(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def ntuDepleted(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
def suppliedWithNtu(details: BuildingWrapper): Behavior[Command] = {
Behaviors.same
}
/**
* Setting the faction on a warp gate is dicey at best
* since the formal logic that controls warp gate faction affiliation is entirely dependent on connectivity.
* The majority of warp gates are connected in pairs and both gates must possess the same faction affinity,
* @param details package class that conveys the important information
* @param faction the faction affiliation to which the facility will update
* @return the next behavior for this control agency messaging system
*/
def setFactionTo(details: BuildingWrapper, faction: PlanetSideEmpire.Value): Behavior[Command] = {
/*
in reality, the faction of most gates is neutral;
the ability to move through the gates is determined by empire-related broadcast settings;
the broadcast settings are dependent by the combined faction affiliations
of the normal facilities connected to either side of the gate pair;
if a faction is assigned to a gate, however, both gates in a pair must possess the same faction affinity
*/
val warpgate = details.building.asInstanceOf[WarpGate]
if (warpgate.Active && warpgate.Faction != faction) {
val local = warpgate.Neighbours.getOrElse(Nil)
BuildingActor.setFactionOnEntity(details, faction, log)
if (local.isEmpty) {
log(details).error(s"warp gate ${warpgate.Name} isolated from neighborhood; check intercontinental linkage")
} else {
local.foreach { _.Actor ! BuildingActor.AlertToFactionChange(warpgate) }
}
}
Behaviors.same
}
/**
* When a building adjacent to this gate changes its faction affiliation,
* the empire-related broadcast settings of this warp gate also update.
* @param details package class that conveys the important information
* @param building the neighbor facility that has had its faction changed
* @return the next behavior for this control agency messaging system
*/
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
val warpgate = details.building.asInstanceOf[WarpGate]
if (warpgate.Active) {
val local = warpgate.Neighbours.getOrElse(Nil)
/*
output: Building, WarpGate:Us, WarpGate, Building
where ":Us" means `details.building`, and ":Msg" means the caller `building`
it could be "Building:Msg, WarpGate:Us, x, y" or "x, Warpgate:Us, Warpgate:Msg, y"
*/
val (thisBuilding, thisWarpGate, otherWarpGate, otherBuilding) = if (local.exists {
_ eq building
}) {
building match {
case _ : WarpGate =>
(
findNeighborhoodNormalBuilding(local), Some(warpgate),
Some(building), findNeighborhoodNormalBuilding(building.Neighbours.getOrElse(Nil))
)
case _ =>
findNeighborhoodWarpGate(local) match {
case out@Some(gate) =>
(
Some(building), Some(warpgate),
out, findNeighborhoodNormalBuilding(gate.Neighbours.getOrElse(Nil))
)
case None =>
(Some(building), Some(warpgate), None, None)
}
}
}
else {
(None, None, None, None)
}
(thisBuilding, thisWarpGate, otherWarpGate, otherBuilding) match {
case (Some(bldg), Some(wg : WarpGate), Some(otherWg : WarpGate), Some(otherBldg)) =>
//standard case where a building connected to a warp gate pair changes faction
val bldgFaction = bldg.Faction
val otherBldgFaction = otherBldg.Faction
val setBroadcastTo = if (Config.app.game.warpGates.broadcastBetweenConflictedFactions) {
Set(bldgFaction, otherBldgFaction)
}
else if (bldgFaction == otherBldgFaction) {
Set(bldgFaction)
}
else {
Set(PlanetSideEmpire.NEUTRAL)
}
updateBroadcastCapabilitiesOfWarpGate(details, wg, setBroadcastTo)
updateBroadcastCapabilitiesOfWarpGate(details, otherWg, setBroadcastTo)
if (wg.Zone.map.cavern && !otherWg.Zone.map.cavern) {
otherWg.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
case (Some(_), Some(wg : WarpGate), Some(otherWg : WarpGate), None) =>
handleWarpGateDeadendPair(details, wg, otherWg)
case (None, Some(wg : WarpGate), Some(otherWg : WarpGate), Some(_)) =>
handleWarpGateDeadendPair(details, otherWg, wg)
case (_, Some(wg: WarpGate), None, None) if !wg.Active =>
updateBroadcastCapabilitiesOfWarpGate(details, wg, Set(PlanetSideEmpire.NEUTRAL))
case (None, None, Some(wg: WarpGate), _) if !wg.Active =>
updateBroadcastCapabilitiesOfWarpGate(details, wg, Set(PlanetSideEmpire.NEUTRAL))
case _ => ;
//everything else is a degenerate pattern that should have been reported at an earlier point
}
}
Behaviors.same
}
/**
* Do these buildings include a warp gate?
* @param neighborhood a series of buildings of various types
* @return the discovered warp gate
*/
def findNeighborhoodWarpGate(neighborhood: Iterable[Building]): Option[Building] = {
neighborhood.find { _ match { case _: WarpGate => true; case _ => false } }
}
/**
* Do these buildings include any facility that is not a warp gate?
* @param neighborhood a series of buildings of various types
* @return the discovered warp gate
*/
def findNeighborhoodNormalBuilding(neighborhood: Iterable[Building]): Option[Building] = {
neighborhood.find { _ match { case _: WarpGate => false; case _ => true } }
}
/**
* Normally, warp gates are connected to each other in a transcontinental pair.
* Onto either gate is a non-gate facility of some sort.
* The facilities on either side normally influence the gate pair; but,
* in this case, only one side of the pair has a facility connected to it.
* Some warp gates are directed to point to different destination warp gates based on the server's policies.
* Another exception to the gate pair rule is a pure broadcast warp gate.
* @param details package class that conveys the important information
* @param warpgate one side of the warp gate pair (usually "our" side)
* @param otherWarpgate the other side of the warp gate pair
*/
private def handleWarpGateDeadendPair(
details: BuildingWrapper,
warpgate: WarpGate,
otherWarpgate: WarpGate
): Unit = {
//either the terminal warp gate messaged its connected gate, or the connected gate messaged the terminal gate
//make certain the connected gate matches the terminal gate's faction
val otherWarpgateFaction = otherWarpgate.Faction
if (warpgate.Faction != otherWarpgateFaction) {
warpgate.Faction = otherWarpgateFaction
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(warpgate.infoUpdateMessage()))
}
//can not be considered broadcast for other factions
val wgBroadcastAllowances = warpgate.AllowBroadcastFor
if (!wgBroadcastAllowances.contains(PlanetSideEmpire.NEUTRAL) || !wgBroadcastAllowances.contains(otherWarpgateFaction)) {
updateBroadcastCapabilitiesOfWarpGate(details, warpgate, Set(PlanetSideEmpire.NEUTRAL))
}
}
/**
* The broadcast settings of a warp gate are changing
* and updates must be provided to the affected factions.
* @param details package class that conveys the important information
* @param warpgate the warp gate entity
* @param setBroadcastTo factions(s) to which the warp gate is to broadcast going forward
*/
private def updateBroadcastCapabilitiesOfWarpGate(
details: BuildingWrapper,
warpgate: WarpGate,
setBroadcastTo: Set[PlanetSideEmpire.Value]
) : Unit = {
val previousAllowances = warpgate.AllowBroadcastFor
val events = details.galaxyService
val msg = GalaxyAction.UpdateBroadcastPrivileges(
warpgate.Zone.Number, warpgate.MapId, previousAllowances, setBroadcastTo
)
warpgate.AllowBroadcastFor = setBroadcastTo
(setBroadcastTo ++ previousAllowances).foreach { faction =>
events ! GalaxyServiceMessage(faction.toString, msg)
}
}
/**
* Warp gates are limitless sources of nanite transfer units when they are active.
* They will always provide the amount specified.
* @param details package class that conveys the important information
* @param msg the original message that instigated this upoate
* @return the next behavior for this control agency messaging system
*/
def ntu(details: BuildingWrapper, msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
msg match {
case Request(amount, replyTo) =>
//warp gates are an infinite source of nanites
val gate = details.building.asInstanceOf[WarpGate]
replyTo ! Grant(gate, if (gate.Active) amount else 0)
case _ => ;
}
Behaviors.same
}
}

View file

@ -35,6 +35,7 @@ import net.psforever.objects.vital._
import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3} import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3}
import net.psforever.types._ import net.psforever.types._
import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition} import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition}
import net.psforever.objects.serverobject.terminals.tabs._
import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier
import scala.annotation.switch import scala.annotation.switch
@ -1261,11 +1262,33 @@ object GlobalDefinitions {
/* /*
Buildings Buildings
*/ */
val building = new BuildingDefinition(474) { Name = "building" } //borrows object id of entity mainbase1 val amp_station = new BuildingDefinition(45) {
val amp_station = new BuildingDefinition(45) { Name = "amp_station"; SOIRadius = 300 } Name = "amp_station"
val comm_station = new BuildingDefinition(211) { Name = "comm_station"; SOIRadius = 300 } SOIRadius = 300
val comm_station_dsp = new BuildingDefinition(212) { Name = "comm_station_dsp"; SOIRadius = 300 } LatticeLinkBenefit = LatticeBenefit.AmpStation
val cryo_facility = new BuildingDefinition(215) { Name = "cryo_facility"; SOIRadius = 300 } }
val comm_station = new BuildingDefinition(211) {
Name = "comm_station"
SOIRadius = 300
LatticeLinkBenefit = LatticeBenefit.InterlinkFacility
}
val comm_station_dsp = new BuildingDefinition(212) {
Name = "comm_station_dsp"
SOIRadius = 300
LatticeLinkBenefit = LatticeBenefit.DropshipCenter
}
val cryo_facility = new BuildingDefinition(215) {
Name = "cryo_facility"
SOIRadius = 300
LatticeLinkBenefit = LatticeBenefit.BioLaboratory
}
val tech_plant = new BuildingDefinition(852) {
Name = "tech_plant"
SOIRadius = 300
LatticeLinkBenefit = LatticeBenefit.TechnologyPlant
}
val building = new BuildingDefinition(474) { Name = "building" } //borrows object id of entity mainbase1
val vanu_core = new BuildingDefinition(932) { Name = "vanu_core" } val vanu_core = new BuildingDefinition(932) { Name = "vanu_core" }
@ -1293,6 +1316,22 @@ object GlobalDefinitions {
val ceiling_bldg_j = new BuildingDefinition(474) { Name = "ceiling_bldg_j" } //borrows object id of entity mainbase1 val ceiling_bldg_j = new BuildingDefinition(474) { Name = "ceiling_bldg_j" } //borrows object id of entity mainbase1
val ceiling_bldg_z = new BuildingDefinition(474) { Name = "ceiling_bldg_z" } //borrows object id of entity mainbase1 val ceiling_bldg_z = new BuildingDefinition(474) { Name = "ceiling_bldg_z" } //borrows object id of entity mainbase1
val mainbase1 = new BuildingDefinition(474) { Name = "mainbase1" }
val mainbase2 = new BuildingDefinition(475) { Name = "mainbase2" }
val mainbase3 = new BuildingDefinition(476) { Name = "mainbase3" }
val meeting_center_nc = new BuildingDefinition(537) { Name = "meeting_center_nc" }
val meeting_center_tr = new BuildingDefinition(538) { Name = "meeting_center_tr" }
val meeting_center_vs = new BuildingDefinition(539) { Name = "meeting_center_vs" }
val minibase1 = new BuildingDefinition(557) { Name = "minibase1" }
val minibase2 = new BuildingDefinition(558) { Name = "minibase2" }
val minibase3 = new BuildingDefinition(559) { Name = "minibase3" }
val redoubt = new BuildingDefinition(726) { Name = "redoubt"; SOIRadius = 187 }
val tower_a = new BuildingDefinition(869) { Name = "tower_a"; SOIRadius = 50 }
val tower_b = new BuildingDefinition(870) { Name = "tower_b"; SOIRadius = 50 }
val tower_c = new BuildingDefinition(871) { Name = "tower_c"; SOIRadius = 50 }
val vanu_control_point = new BuildingDefinition(931) { Name = "vanu_control_point"; SOIRadius = 187 }
val vanu_vehicle_station = new BuildingDefinition(948) { Name = "vanu_vehicle_station"; SOIRadius = 187 }
val hst = new WarpGateDefinition(402) val hst = new WarpGateDefinition(402)
hst.Name = "hst" hst.Name = "hst"
hst.UseRadius = 20.4810f hst.UseRadius = 20.4810f
@ -1309,23 +1348,6 @@ object GlobalDefinitions {
hst.NoWarp += peregrine_flight hst.NoWarp += peregrine_flight
hst.SpecificPointFunc = SpawnPoint.Gate hst.SpecificPointFunc = SpawnPoint.Gate
val mainbase1 = new BuildingDefinition(474) { Name = "mainbase1" }
val mainbase2 = new BuildingDefinition(475) { Name = "mainbase2" }
val mainbase3 = new BuildingDefinition(476) { Name = "mainbase3" }
val meeting_center_nc = new BuildingDefinition(537) { Name = "meeting_center_nc" }
val meeting_center_tr = new BuildingDefinition(538) { Name = "meeting_center_tr" }
val meeting_center_vs = new BuildingDefinition(539) { Name = "meeting_center_vs" }
val minibase1 = new BuildingDefinition(557) { Name = "minibase1" }
val minibase2 = new BuildingDefinition(558) { Name = "minibase2" }
val minibase3 = new BuildingDefinition(559) { Name = "minibase3" }
val redoubt = new BuildingDefinition(726) { Name = "redoubt"; SOIRadius = 187 }
val tech_plant = new BuildingDefinition(852) { Name = "tech_plant"; SOIRadius = 300 }
val tower_a = new BuildingDefinition(869) { Name = "tower_a"; SOIRadius = 50 }
val tower_b = new BuildingDefinition(870) { Name = "tower_b"; SOIRadius = 50 }
val tower_c = new BuildingDefinition(871) { Name = "tower_c"; SOIRadius = 50 }
val vanu_control_point = new BuildingDefinition(931) { Name = "vanu_control_point"; SOIRadius = 187 }
val vanu_vehicle_station = new BuildingDefinition(948) { Name = "vanu_vehicle_station"; SOIRadius = 187 }
val warpgate = new WarpGateDefinition(993) val warpgate = new WarpGateDefinition(993)
warpgate.Name = "warpgate" warpgate.Name = "warpgate"
warpgate.UseRadius = 301.8713f warpgate.UseRadius = 301.8713f
@ -1338,7 +1360,7 @@ object GlobalDefinitions {
warpgate_cavern.UseRadius = 51.0522f warpgate_cavern.UseRadius = 51.0522f
warpgate_cavern.SOIRadius = 52 warpgate_cavern.SOIRadius = 52
warpgate_cavern.VehicleAllowance = true warpgate_cavern.VehicleAllowance = true
warpgate_cavern.SpecificPointFunc = SpawnPoint.Gate warpgate_cavern.SpecificPointFunc = SpawnPoint.HalfHighGate
val warpgate_small = new WarpGateDefinition(995) val warpgate_small = new WarpGateDefinition(995)
warpgate_small.Name = "warpgate_small" warpgate_small.Name = "warpgate_small"
@ -3466,7 +3488,7 @@ object GlobalDefinitions {
maelstrom_grenade_damager.Name = "maelstrom_grenade_damager" maelstrom_grenade_damager.Name = "maelstrom_grenade_damager"
maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct
//todo the maelstrom_grenade_damage is something of a broken entity atm //the maelstrom_grenade_damage is something of a broken entity atm
maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile" maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile"
maelstrom_grenade_projectile.Damage0 = 32 maelstrom_grenade_projectile.Damage0 = 32
@ -3953,7 +3975,7 @@ object GlobalDefinitions {
ProjectileDefinition.CalculateDerivedFields(quasar_projectile) ProjectileDefinition.CalculateDerivedFields(quasar_projectile)
radiator_cloud.Name = "radiator_cloud" radiator_cloud.Name = "radiator_cloud"
radiator_cloud.Damage0 = 2 radiator_cloud.Damage0 = 1 //2
radiator_cloud.DamageAtEdge = 1.0f radiator_cloud.DamageAtEdge = 1.0f
radiator_cloud.DamageRadius = 5f radiator_cloud.DamageRadius = 5f
radiator_cloud.DamageToHealthOnly = true radiator_cloud.DamageToHealthOnly = true
@ -5106,7 +5128,6 @@ object GlobalDefinitions {
spiker.FireModes.head.AmmoSlotIndex = 0 spiker.FireModes.head.AmmoSlotIndex = 0
spiker.FireModes.head.Magazine = 25 spiker.FireModes.head.Magazine = 25
spiker.Tile = InventoryTile.Tile33 spiker.Tile = InventoryTile.Tile33
//TODO the spiker is weird
mini_chaingun.Name = "mini_chaingun" mini_chaingun.Name = "mini_chaingun"
mini_chaingun.Size = EquipmentSize.Rifle mini_chaingun.Size = EquipmentSize.Rifle
@ -5182,7 +5203,6 @@ object GlobalDefinitions {
maelstrom.FireModes(2).Magazine = 150 maelstrom.FireModes(2).Magazine = 150
maelstrom.FireModes(2).RoundsPerShot = 10 maelstrom.FireModes(2).RoundsPerShot = 10
maelstrom.Tile = InventoryTile.Tile93 maelstrom.Tile = InventoryTile.Tile93
//TODO the maelstrom is weird
phoenix.Name = "phoenix" phoenix.Name = "phoenix"
phoenix.Size = EquipmentSize.Rifle phoenix.Size = EquipmentSize.Rifle
@ -8835,7 +8855,7 @@ object GlobalDefinitions {
colossus_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L) colossus_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f) colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f)
colossus_flight.MaxCapacitor = 156 colossus_flight.MaxCapacitor = 156
colossus_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor colossus_flight.DefaultCapacitor = colossus_flight.MaxCapacitor
colossus_flight.CapacitorDrain = 16 colossus_flight.CapacitorDrain = 16
colossus_flight.CapacitorDrainSpecial = 3 colossus_flight.CapacitorDrainSpecial = 3
colossus_flight.CapacitorRecharge = 42 colossus_flight.CapacitorRecharge = 42
@ -8889,7 +8909,7 @@ object GlobalDefinitions {
peregrine_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L) peregrine_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f) peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f)
peregrine_flight.MaxCapacitor = 156 peregrine_flight.MaxCapacitor = 156
peregrine_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor peregrine_flight.DefaultCapacitor = peregrine_flight.MaxCapacitor
peregrine_flight.CapacitorDrain = 16 peregrine_flight.CapacitorDrain = 16
peregrine_flight.CapacitorDrainSpecial = 3 peregrine_flight.CapacitorDrainSpecial = 3
peregrine_flight.CapacitorRecharge = 42 peregrine_flight.CapacitorRecharge = 42
@ -9303,21 +9323,29 @@ object GlobalDefinitions {
spawn_terminal.Name = "spawn_terminal" spawn_terminal.Name = "spawn_terminal"
spawn_terminal.Damageable = false spawn_terminal.Damageable = false
spawn_terminal.Repairable = false spawn_terminal.Repairable = false
spawn_terminal.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default value? spawn_terminal.autoRepair = AutoRepairStats(1, 5000, 200, 1)
order_terminal.Name = "order_terminal" order_terminal.Name = "order_terminal"
order_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( order_terminal.Tab += 0 -> {
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons val tab = EquipmentPage(
) EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
order_terminal.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage( )
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
order_terminal.Tab += 1 -> ArmorWithAmmoPage(
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits, EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
EquipmentTerminalDefinition.maxAmmo EquipmentTerminalDefinition.maxAmmo
) )
order_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( order_terminal.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
) )
order_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
order_terminal.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminal.Tab += 4 -> {
val tab = InfantryLoadoutPage()
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
order_terminal.SellEquipmentByDefault = true order_terminal.SellEquipmentByDefault = true
order_terminal.MaxHealth = 500 order_terminal.MaxHealth = 500
order_terminal.Damageable = true order_terminal.Damageable = true
@ -9328,61 +9356,75 @@ object GlobalDefinitions {
order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f) order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f)
order_terminala.Name = "order_terminala" order_terminala.Name = "order_terminala"
order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( order_terminala.Tab += 0 -> {
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons val tab = EquipmentPage(
) EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
order_terminala.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage( )
tab.Exclude = List(NoCavernEquipmentRule)
tab
}
order_terminala.Tab += 1 -> ArmorWithAmmoPage(
EquipmentTerminalDefinition.suits, EquipmentTerminalDefinition.suits,
EquipmentTerminalDefinition.maxAmmo EquipmentTerminalDefinition.maxAmmo
) )
order_terminala.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( order_terminala.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
) )
order_terminala.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminala.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
order_terminala.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminala.Tab += 4 -> {
order_terminala.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX val tab = InfantryLoadoutPage()
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
tab
}
order_terminala.SellEquipmentByDefault = true order_terminala.SellEquipmentByDefault = true
order_terminala.Damageable = false order_terminala.Damageable = false
order_terminala.Repairable = false order_terminala.Repairable = false
order_terminalb.Name = "order_terminalb" order_terminalb.Name = "order_terminalb"
order_terminalb.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( order_terminalb.Tab += 0 -> {
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons val tab = EquipmentPage(
) EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
order_terminalb.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage( )
tab.Exclude = List(NoCavernEquipmentRule)
tab
}
order_terminalb.Tab += 1 -> ArmorWithAmmoPage(
EquipmentTerminalDefinition.suits, EquipmentTerminalDefinition.suits,
EquipmentTerminalDefinition.maxAmmo EquipmentTerminalDefinition.maxAmmo
) )
order_terminalb.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( order_terminalb.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
) )
order_terminalb.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminalb.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
order_terminalb.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminalb.Tab += 4 -> {
order_terminalb.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX val tab = InfantryLoadoutPage()
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
tab
}
order_terminalb.SellEquipmentByDefault = true order_terminalb.SellEquipmentByDefault = true
order_terminalb.Damageable = false order_terminalb.Damageable = false
order_terminalb.Repairable = false order_terminalb.Repairable = false
vanu_equipment_term.Name = "vanu_equipment_term" vanu_equipment_term.Name = "vanu_equipment_term"
vanu_equipment_term.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( vanu_equipment_term.Tab += 0 -> EquipmentPage(
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
) )
vanu_equipment_term.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage( vanu_equipment_term.Tab += 1 -> ArmorWithAmmoPage(
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits, EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
EquipmentTerminalDefinition.maxAmmo EquipmentTerminalDefinition.maxAmmo
) )
vanu_equipment_term.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( vanu_equipment_term.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
) )
vanu_equipment_term.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) vanu_equipment_term.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
vanu_equipment_term.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() vanu_equipment_term.Tab += 4 -> InfantryLoadoutPage()
vanu_equipment_term.SellEquipmentByDefault = true vanu_equipment_term.SellEquipmentByDefault = true
vanu_equipment_term.Damageable = false vanu_equipment_term.Damageable = false
vanu_equipment_term.Repairable = false vanu_equipment_term.Repairable = false
cert_terminal.Name = "cert_terminal" cert_terminal.Name = "cert_terminal"
val certs = Certification.values.filter(_.cost != 0) val certs = Certification.values.filter(_.cost != 0)
val page = OrderTerminalDefinition.CertificationPage(certs) val page = CertificationPage(certs)
cert_terminal.Tab += 0 -> page cert_terminal.Tab += 0 -> page
cert_terminal.MaxHealth = 500 cert_terminal.MaxHealth = 500
cert_terminal.Damageable = true cert_terminal.Damageable = true
@ -9402,20 +9444,28 @@ object GlobalDefinitions {
implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f) implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f)
implant_terminal_interface.Name = "implant_terminal_interface" implant_terminal_interface.Name = "implant_terminal_interface"
implant_terminal_interface.Tab += 0 -> OrderTerminalDefinition.ImplantPage(ImplantTerminalDefinition.implants) implant_terminal_interface.Tab += 0 -> ImplantPage(ImplantTerminalDefinition.implants)
implant_terminal_interface.MaxHealth = 500 implant_terminal_interface.MaxHealth = 500
implant_terminal_interface.Damageable = false //TODO true implant_terminal_interface.Damageable = false //TODO true
implant_terminal_interface.Repairable = true implant_terminal_interface.Repairable = true
implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default value? implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1)
implant_terminal_interface.RepairIfDestroyed = true implant_terminal_interface.RepairIfDestroyed = true
//TODO will need geometry when Damageable = true //TODO will need geometry when Damageable = true
ground_vehicle_terminal.Name = "ground_vehicle_terminal" ground_vehicle_terminal.Name = "ground_vehicle_terminal"
ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( ground_vehicle_terminal.Tab += 46769 -> {
VehicleTerminalDefinition.groundVehicles, val tab = VehiclePage(
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.groundVehicles,
) VehicleTerminalDefinition.trunk
ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) )
tab.Exclude = List(CavernVehicleQuestion)
tab
}
ground_vehicle_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
ground_vehicle_terminal.MaxHealth = 500 ground_vehicle_terminal.MaxHealth = 500
ground_vehicle_terminal.Damageable = true ground_vehicle_terminal.Damageable = true
ground_vehicle_terminal.Repairable = true ground_vehicle_terminal.Repairable = true
@ -9425,11 +9475,15 @@ object GlobalDefinitions {
ground_vehicle_terminal.Geometry = vterm ground_vehicle_terminal.Geometry = vterm
air_vehicle_terminal.Name = "air_vehicle_terminal" air_vehicle_terminal.Name = "air_vehicle_terminal"
air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( air_vehicle_terminal.Tab += 46769 -> VehiclePage(
VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.flight1Vehicles,
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.trunk
) )
air_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) air_vehicle_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernVehicleQuestion, CavernEquipmentQuestion)
tab
}
air_vehicle_terminal.MaxHealth = 500 air_vehicle_terminal.MaxHealth = 500
air_vehicle_terminal.Damageable = true air_vehicle_terminal.Damageable = true
air_vehicle_terminal.Repairable = true air_vehicle_terminal.Repairable = true
@ -9439,11 +9493,15 @@ object GlobalDefinitions {
air_vehicle_terminal.Geometry = vterm air_vehicle_terminal.Geometry = vterm
dropship_vehicle_terminal.Name = "dropship_vehicle_terminal" dropship_vehicle_terminal.Name = "dropship_vehicle_terminal"
dropship_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( dropship_vehicle_terminal.Tab += 46769 -> VehiclePage(
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles, VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles,
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.trunk
) )
dropship_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) dropship_vehicle_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
dropship_vehicle_terminal.MaxHealth = 500 dropship_vehicle_terminal.MaxHealth = 500
dropship_vehicle_terminal.Damageable = true dropship_vehicle_terminal.Damageable = true
dropship_vehicle_terminal.Repairable = true dropship_vehicle_terminal.Repairable = true
@ -9453,11 +9511,19 @@ object GlobalDefinitions {
dropship_vehicle_terminal.Geometry = vterm dropship_vehicle_terminal.Geometry = vterm
vehicle_terminal_combined.Name = "vehicle_terminal_combined" vehicle_terminal_combined.Name = "vehicle_terminal_combined"
vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( vehicle_terminal_combined.Tab += 46769 -> {
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles, val tab = VehiclePage(
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles,
) VehicleTerminalDefinition.trunk
vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) )
tab.Exclude = List(CavernVehicleQuestion)
tab
}
vehicle_terminal_combined.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
vehicle_terminal_combined.MaxHealth = 500 vehicle_terminal_combined.MaxHealth = 500
vehicle_terminal_combined.Damageable = true vehicle_terminal_combined.Damageable = true
vehicle_terminal_combined.Repairable = true vehicle_terminal_combined.Repairable = true
@ -9467,11 +9533,11 @@ object GlobalDefinitions {
vehicle_terminal_combined.Geometry = vterm vehicle_terminal_combined.Geometry = vterm
vanu_air_vehicle_term.Name = "vanu_air_vehicle_term" vanu_air_vehicle_term.Name = "vanu_air_vehicle_term"
vanu_air_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( vanu_air_vehicle_term.Tab += 46769 -> VehiclePage(
VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.flight1Vehicles,
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.trunk
) )
vanu_air_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) vanu_air_vehicle_term.Tab += 4 -> VehicleLoadoutPage(10)
vanu_air_vehicle_term.MaxHealth = 500 vanu_air_vehicle_term.MaxHealth = 500
vanu_air_vehicle_term.Damageable = true vanu_air_vehicle_term.Damageable = true
vanu_air_vehicle_term.Repairable = true vanu_air_vehicle_term.Repairable = true
@ -9480,11 +9546,11 @@ object GlobalDefinitions {
vanu_air_vehicle_term.Subtract.Damage1 = 8 vanu_air_vehicle_term.Subtract.Damage1 = 8
vanu_vehicle_term.Name = "vanu_vehicle_term" vanu_vehicle_term.Name = "vanu_vehicle_term"
vanu_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( vanu_vehicle_term.Tab += 46769 -> VehiclePage(
VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.groundVehicles,
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.trunk
) )
vanu_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) vanu_vehicle_term.Tab += 4 -> VehicleLoadoutPage(10)
vanu_vehicle_term.MaxHealth = 500 vanu_vehicle_term.MaxHealth = 500
vanu_vehicle_term.Damageable = true vanu_vehicle_term.Damageable = true
vanu_vehicle_term.Repairable = true vanu_vehicle_term.Repairable = true
@ -9493,19 +9559,21 @@ object GlobalDefinitions {
vanu_vehicle_term.Subtract.Damage1 = 8 vanu_vehicle_term.Subtract.Damage1 = 8
bfr_terminal.Name = "bfr_terminal" bfr_terminal.Name = "bfr_terminal"
bfr_terminal.Tab += 0 -> OrderTerminalDefinition.VehiclePage( bfr_terminal.Tab += 0 -> VehiclePage(
VehicleTerminalDefinition.bfrVehicles, VehicleTerminalDefinition.bfrVehicles,
VehicleTerminalDefinition.trunk VehicleTerminalDefinition.trunk
) )
bfr_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage( bfr_terminal.Tab += 1 -> EquipmentPage(
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
) //inaccessible? ) //inaccessible?
bfr_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( bfr_terminal.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
) //inaccessible? ) //inaccessible?
bfr_terminal.Tab += 3 -> OrderTerminalDefinition.BattleframeSpawnLoadoutPage( bfr_terminal.Tab += 3 -> {
VehicleTerminalDefinition.bfrVehicles val tab = BattleframeSpawnLoadoutPage(VehicleTerminalDefinition.bfrVehicles)
) tab.Exclude = List(CavernEquipmentQuestion)
tab
}
bfr_terminal.MaxHealth = 500 bfr_terminal.MaxHealth = 500
bfr_terminal.Damageable = true bfr_terminal.Damageable = true
bfr_terminal.Repairable = true bfr_terminal.Repairable = true
@ -9521,7 +9589,7 @@ object GlobalDefinitions {
respawn_tube.Damageable = true respawn_tube.Damageable = true
respawn_tube.DamageableByFriendlyFire = false respawn_tube.DamageableByFriendlyFire = false
respawn_tube.Repairable = true respawn_tube.Repairable = true
respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value? respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
respawn_tube.RepairIfDestroyed = true respawn_tube.RepairIfDestroyed = true
respawn_tube.Subtract.Damage1 = 8 respawn_tube.Subtract.Damage1 = 8
respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
@ -9533,7 +9601,7 @@ object GlobalDefinitions {
respawn_tube_sanctuary.Damageable = false //true? respawn_tube_sanctuary.Damageable = false //true?
respawn_tube_sanctuary.DamageableByFriendlyFire = false respawn_tube_sanctuary.DamageableByFriendlyFire = false
respawn_tube_sanctuary.Repairable = true respawn_tube_sanctuary.Repairable = true
respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value? respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
//TODO will need geometry when Damageable = true //TODO will need geometry when Damageable = true
respawn_tube_tower.Name = "respawn_tube_tower" respawn_tube_tower.Name = "respawn_tube_tower"
@ -9543,18 +9611,18 @@ object GlobalDefinitions {
respawn_tube_tower.Damageable = true respawn_tube_tower.Damageable = true
respawn_tube_tower.DamageableByFriendlyFire = false respawn_tube_tower.DamageableByFriendlyFire = false
respawn_tube_tower.Repairable = true respawn_tube_tower.Repairable = true
respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value? respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
respawn_tube_tower.RepairIfDestroyed = true respawn_tube_tower.RepairIfDestroyed = true
respawn_tube_tower.Subtract.Damage1 = 8 respawn_tube_tower.Subtract.Damage1 = 8
respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
teleportpad_terminal.Name = "teleportpad_terminal" teleportpad_terminal.Name = "teleportpad_terminal"
teleportpad_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.routerTerminal) teleportpad_terminal.Tab += 0 -> EquipmentPage(EquipmentTerminalDefinition.routerTerminal)
teleportpad_terminal.Damageable = false teleportpad_terminal.Damageable = false
teleportpad_terminal.Repairable = false teleportpad_terminal.Repairable = false
targeting_laser_dispenser.Name = "targeting_laser_dispenser" targeting_laser_dispenser.Name = "targeting_laser_dispenser"
targeting_laser_dispenser.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.flailTerminal) targeting_laser_dispenser.Tab += 0 -> EquipmentPage(EquipmentTerminalDefinition.flailTerminal)
targeting_laser_dispenser.Damageable = false targeting_laser_dispenser.Damageable = false
targeting_laser_dispenser.Repairable = false targeting_laser_dispenser.Repairable = false
@ -9791,38 +9859,54 @@ object GlobalDefinitions {
lodestar_repair_terminal.Repairable = false lodestar_repair_terminal.Repairable = false
multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal" multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal"
multivehicle_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage( multivehicle_rearm_terminal.Tab += 3 -> EquipmentPage(
EquipmentTerminalDefinition.vehicleAmmunition EquipmentTerminalDefinition.vehicleAmmunition
) )
multivehicle_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) multivehicle_rearm_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ? multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ?
multivehicle_rearm_terminal.Damageable = false multivehicle_rearm_terminal.Damageable = false
multivehicle_rearm_terminal.Repairable = false multivehicle_rearm_terminal.Repairable = false
bfr_rearm_terminal.Name = "bfr_rearm_terminal" bfr_rearm_terminal.Name = "bfr_rearm_terminal"
bfr_rearm_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage( bfr_rearm_terminal.Tab += 1 -> EquipmentPage(
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
) )
bfr_rearm_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( bfr_rearm_terminal.Tab += 2 -> EquipmentPage(
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
) )
bfr_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.VehicleLoadoutPage(15) bfr_rearm_terminal.Tab += 3 -> {
val tab = VehicleLoadoutPage(15)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ? bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ?
bfr_rearm_terminal.Damageable = false bfr_rearm_terminal.Damageable = false
bfr_rearm_terminal.Repairable = false bfr_rearm_terminal.Repairable = false
air_rearm_terminal.Name = "air_rearm_terminal" air_rearm_terminal.Name = "air_rearm_terminal"
air_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) air_rearm_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) air_rearm_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
air_rearm_terminal.SellEquipmentByDefault = true //TODO ? air_rearm_terminal.SellEquipmentByDefault = true //TODO ?
air_rearm_terminal.Damageable = false air_rearm_terminal.Damageable = false
air_rearm_terminal.Repairable = false air_rearm_terminal.Repairable = false
ground_rearm_terminal.Name = "ground_rearm_terminal" ground_rearm_terminal.Name = "ground_rearm_terminal"
ground_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage( ground_rearm_terminal.Tab += 3 -> EquipmentPage(
EquipmentTerminalDefinition.vehicleAmmunition EquipmentTerminalDefinition.vehicleAmmunition
) )
ground_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) ground_rearm_terminal.Tab += 4 -> {
val tab = VehicleLoadoutPage(10)
tab.Exclude = List(CavernEquipmentQuestion)
tab
}
ground_rearm_terminal.SellEquipmentByDefault = true //TODO ? ground_rearm_terminal.SellEquipmentByDefault = true //TODO ?
ground_rearm_terminal.Damageable = false ground_rearm_terminal.Damageable = false
ground_rearm_terminal.Repairable = false ground_rearm_terminal.Repairable = false
@ -9942,7 +10026,7 @@ object GlobalDefinitions {
generator.Damageable = true generator.Damageable = true
generator.DamageableByFriendlyFire = false generator.DamageableByFriendlyFire = false
generator.Repairable = true generator.Repairable = true
generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) //TODO drain is default value? generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1)
generator.RepairDistance = 13.5f generator.RepairDistance = 13.5f
generator.RepairIfDestroyed = true generator.RepairIfDestroyed = true
generator.Subtract.Damage1 = 9 generator.Subtract.Damage1 = 9

View file

@ -212,8 +212,12 @@ class Player(var avatar: Avatar)
def HolsterItems(): List[InventoryItem] = holsters def HolsterItems(): List[InventoryItem] = holsters
.zipWithIndex .zipWithIndex
.collect { .collect {
case (slot: EquipmentSlot, index: Int) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index) case (slot: EquipmentSlot, index: Int) =>
}.toList slot.Equipment match {
case Some(item) => Some(InventoryItem(item, index))
case None => None
}
}.flatten.toList
def Inventory: GridInventory = inventory def Inventory: GridInventory = inventory

View file

@ -124,6 +124,16 @@ object SpawnPoint {
Default(obj, target) Default(obj, target)
} }
} }
def HalfHighGate(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = {
val (a, b) = Gate(obj, target)
target match {
case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) =>
(a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b)
case _ =>
(a, b)
}
}
} }
trait SpawnPointDefinition { trait SpawnPointDefinition {

View file

@ -181,7 +181,7 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
zone.GUID(obj.Telepad) match { zone.GUID(obj.Telepad) match {
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled => case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
oldTpad.Actor ! TelepadLike.SeverLink(obj) oldTpad.Actor ! TelepadLike.SeverLink(obj)
case None => ; case _ => ;
} }
obj.Telepad = tpad.GUID obj.Telepad = tpad.GUID
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj)) //zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))

View file

@ -253,7 +253,7 @@ class GridInventory extends Container {
Success(collisions.toList) Success(collisions.toList)
} catch { } catch {
case e: NoSuchElementException => case e: NoSuchElementException =>
Failure(InventoryDisarrayException(s"inventory contained old item data", e)) Failure(InventoryDisarrayException(s"inventory contained old item data", e, this))
case e: Exception => case e: Exception =>
Failure(e) Failure(e)
} }

View file

@ -10,7 +10,11 @@ package net.psforever.objects.inventory
* @param message the explanation of why the exception was thrown * @param message the explanation of why the exception was thrown
* @param cause any prior `Exception` that was thrown then wrapped in this one * @param cause any prior `Exception` that was thrown then wrapped in this one
*/ */
final case class InventoryDisarrayException(private val message: String = "", private val cause: Throwable) final case class InventoryDisarrayException(
private val message: String,
private val cause: Throwable,
inventory: GridInventory
)
extends Exception(message, cause) extends Exception(message, cause)
object InventoryDisarrayException { object InventoryDisarrayException {
@ -21,6 +25,6 @@ object InventoryDisarrayException {
* @param message the explanation of why the exception was thrown * @param message the explanation of why the exception was thrown
* @return an `InventoryDisarrayException` object * @return an `InventoryDisarrayException` object
*/ */
def apply(message: String): InventoryDisarrayException = def apply(message: String, inventory: GridInventory): InventoryDisarrayException =
InventoryDisarrayException(message, None.orNull) InventoryDisarrayException(message, None.orNull, inventory)
} }

View file

@ -652,10 +652,11 @@ object ContainableBehavior {
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped. * A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
* Used to filter through lists of object data before it is placed into a player's inventory. * Used to filter through lists of object data before it is placed into a player's inventory.
* Drop the item if:<br> * Drop the item if:<br>
* - the item is cavern equipment<br>
* - the item is a `BoomerTrigger` type object<br> * - the item is a `BoomerTrigger` type object<br>
* - the item is a `router_telepad` type object<br> * - the item is a `router_telepad` type object<br>
* - the item is another faction's exclusive equipment * - the item is another faction's exclusive equipment<br>
* Additional equipment filtration information can be found attached to terminals.
* @see `ExclusionRule`
* @param tplayer the player * @param tplayer the player
* @return true if the item is to be dropped; false, otherwise * @return true if the item is to be dropped; false, otherwise
*/ */
@ -663,7 +664,6 @@ object ContainableBehavior {
entry => { entry => {
val objDef = entry.obj.Definition val objDef = entry.obj.Definition
val faction = GlobalDefinitions.isFactionEquipment(objDef) val faction = GlobalDefinitions.isFactionEquipment(objDef)
GlobalDefinitions.isCavernEquipment(objDef) ||
objDef == GlobalDefinitions.router_telepad || objDef == GlobalDefinitions.router_telepad ||
entry.obj.isInstanceOf[BoomerTrigger] || entry.obj.isInstanceOf[BoomerTrigger] ||
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)

View file

@ -6,7 +6,6 @@ import java.util.concurrent.TimeUnit
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.actors.zone.BuildingActor import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player} import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player}
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.painbox.Painbox import net.psforever.objects.serverobject.painbox.Painbox
@ -15,7 +14,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.packet.game.BuildingInfoUpdateMessage
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
@ -88,10 +87,19 @@ class Building(
} }
// Get all lattice neighbours // Get all lattice neighbours
def Neighbours: Option[Set[Building]] = { def AllNeighbours: Option[Set[Building]] = {
zone.Lattice find this match { zone.Lattice find this match {
case Some(x) => Some(x.diSuccessors.map(x => x.toOuter)) case Some(x) => Some(x.diSuccessors.map(x => x.toOuter))
case None => None; case None => None
}
}
// Get all lattice neighbours that are active
// This is important because warp gates can be inactive
def Neighbours: Option[Set[Building]] = {
AllNeighbours collect {
case wg: WarpGate if wg.Active => wg
case b => b
} }
} }
@ -189,6 +197,15 @@ class Building(
val o = Amenities.collect({ case tube: SpawnTube if !tube.Destroyed => tube }) val o = Amenities.collect({ case tube: SpawnTube if !tube.Destroyed => tube })
(o.nonEmpty, false) //TODO poll pain field strength (o.nonEmpty, false) //TODO poll pain field strength
} }
val cavernBenefit: Set[CavernBenefit] = if (
generatorState != PlanetSideGeneratorState.Destroyed &&
faction != PlanetSideEmpire.NEUTRAL &&
connectedCavern().nonEmpty
) {
Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule)
} else {
Set(CavernBenefit.None)
}
BuildingInfoUpdateMessage( BuildingInfoUpdateMessage(
Zone.Number, Zone.Number,
@ -198,25 +215,29 @@ class Building(
hackingFaction, hackingFaction,
hackTime, hackTime,
if (ntuLevel > 0) Faction else PlanetSideEmpire.NEUTRAL, if (ntuLevel > 0) Faction else PlanetSideEmpire.NEUTRAL,
0, // unk1 Field != 0 will cause malformed packet unk1 = 0, // unk1 != 0 will cause malformed packet
None, // unk1x unk1x = None,
generatorState, generatorState,
spawnTubesNormal, spawnTubesNormal,
forceDomeActive, forceDomeActive,
if (generatorState != PlanetSideGeneratorState.Destroyed) latticeBenefitsValue() else 0, latticeConnectedFacilityBenefits(),
if (generatorState != PlanetSideGeneratorState.Destroyed) 48 else 0, // cavern benefit cavernBenefit,
Nil, // unk4, unk4 = Nil,
0, // unk5 unk5 = 0,
false, // unk6 unk6 = false,
8, // unk7 Field != 8 will cause malformed packet unk7 = 8, // unk7 != 8 will cause malformed packet
None, // unk7x unk7x = None,
boostSpawnPain, boostSpawnPain,
boostGeneratorPain boostGeneratorPain
) )
} }
def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = { def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
if (Faction == PlanetSideEmpire.NEUTRAL) { val genState = Generator match {
case Some(obj) => obj.Condition != PlanetSideGeneratorState.Destroyed
case _ => false
}
if (genState || Faction == PlanetSideEmpire.NEUTRAL) {
false false
} else { } else {
// Check this Building is on the lattice first // Check this Building is on the lattice first
@ -237,16 +258,16 @@ class Building(
} }
private def findLatticeBenefit( private def findLatticeBenefit(
wantedBenefit: ObjectDefinition, wantedBenefit: LatticeBenefit,
subGraph: Graph[Building, GraphEdge.UnDiEdge] subGraph: Graph[Building, GraphEdge.UnDiEdge]
): Boolean = { ): Boolean = {
var found = false var found = false
subGraph find this match { subGraph find this match {
case Some(self) => case Some(self) =>
if (this.Definition == wantedBenefit) { if (this.Definition.LatticeLinkBenefit == wantedBenefit) {
found = true found = true
} else { } else {
self pathUntil (_.Definition == wantedBenefit) match { self pathUntil (_.Definition.LatticeLinkBenefit == wantedBenefit) match {
case Some(_) => found = true case Some(_) => found = true
case None => ; case None => ;
} }
@ -256,54 +277,60 @@ class Building(
found found
} }
def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = { def latticeConnectedFacilityBenefits(): Set[LatticeBenefit] = {
if (Faction == PlanetSideEmpire.NEUTRAL) { val genState = Generator match {
Set.empty case Some(obj) => obj.Condition
case _ => PlanetSideGeneratorState.Normal
}
if (genState == PlanetSideGeneratorState.Destroyed || Faction == PlanetSideEmpire.NEUTRAL) {
Set(LatticeBenefit.None)
} else { } else {
// Check this Building is on the lattice first friendlyFunctionalNeighborhood().map { _.Definition.LatticeLinkBenefit }
zone.Lattice find this match {
case Some(_) =>
val subGraph = Zone.Lattice filter ((b: Building) =>
b.Faction == this.Faction
&& !b.CaptureTerminalIsHacked
&& b.NtuLevel > 0
&& (b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed)
)
import scala.collection.mutable
var connectedBases: mutable.Set[ObjectDefinition] = mutable.Set()
if (findLatticeBenefit(GlobalDefinitions.amp_station, subGraph)) {
connectedBases.add(GlobalDefinitions.amp_station)
}
if (findLatticeBenefit(GlobalDefinitions.comm_station_dsp, subGraph)) {
connectedBases.add(GlobalDefinitions.comm_station_dsp)
}
if (findLatticeBenefit(GlobalDefinitions.cryo_facility, subGraph)) {
connectedBases.add(GlobalDefinitions.cryo_facility)
}
if (findLatticeBenefit(GlobalDefinitions.comm_station, subGraph)) {
connectedBases.add(GlobalDefinitions.comm_station)
}
if (findLatticeBenefit(GlobalDefinitions.tech_plant, subGraph)) {
connectedBases.add(GlobalDefinitions.tech_plant)
}
connectedBases.toSet
case None =>
Set.empty
}
} }
} }
def latticeBenefitsValue(): Int = { def friendlyFunctionalNeighborhood(): Set[Building] = {
latticeConnectedFacilityBenefits().collect { var (currBuilding, newNeighbors) = Neighbours(faction).getOrElse(Set.empty[Building]).toList.splitAt(1)
case GlobalDefinitions.amp_station => 1 var visitedNeighbors: Set[Int] = Set(MapId)
case GlobalDefinitions.comm_station_dsp => 2 var friendlyNeighborhood: List[Building] = List(this)
case GlobalDefinitions.cryo_facility => 4 while (currBuilding.nonEmpty) {
case GlobalDefinitions.comm_station => 8 val building = currBuilding.head
case GlobalDefinitions.tech_plant => 16 val neighborsToAdd = if (!visitedNeighbors.contains(building.MapId)
}.sum && (building match { case _ : WarpGate => false; case _ => true })
&& !building.CaptureTerminalIsHacked
&& building.NtuLevel > 0
&& (building.Generator match {
case Some(o) => o.Condition != PlanetSideGeneratorState.Destroyed
case _ => true
})
) {
visitedNeighbors = visitedNeighbors ++ Set(building.MapId)
friendlyNeighborhood = friendlyNeighborhood :+ building
building.Neighbours(faction)
.getOrElse(Set.empty[Building])
.toList
.filterNot { b => visitedNeighbors.contains(b.MapId) }
} else {
Nil
}
val allocatedNeighbors = newNeighbors ++ neighborsToAdd
currBuilding = allocatedNeighbors.take(1)
newNeighbors = allocatedNeighbors.drop(1)
}
friendlyNeighborhood.toSet
} }
/**
* Starting from an overworld zone facility,
* find a lattice connected cavern facility that is the same faction as this starting building.
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
* do not let the search escape the current zone into another.
* If we start in a cavern zone, do not continue a fruitless search;
* just fail.
* @return the discovered faction-aligned cavern facility
*/
def connectedCavern(): Option[Building] = net.psforever.objects.zones.Zone.findConnectedCavernFacility(building = this)
def BuildingType: StructureType = buildingType def BuildingType: StructureType = buildingType
override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor

View file

@ -2,10 +2,30 @@ package net.psforever.objects.serverobject.structures
import net.psforever.objects.{NtuContainerDefinition, SpawnPointDefinition} import net.psforever.objects.{NtuContainerDefinition, SpawnPointDefinition}
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
import net.psforever.types.{CaptureBenefit, CavernBenefit, LatticeBenefit}
class BuildingDefinition(objectId: Int) extends ObjectDefinition(objectId) with NtuContainerDefinition with SphereOfInfluence { class BuildingDefinition(objectId: Int)
extends ObjectDefinition(objectId)
with NtuContainerDefinition
with SphereOfInfluence {
Name = "building" Name = "building"
MaxNtuCapacitor = Int.MaxValue MaxNtuCapacitor = Int.MaxValue
private var latBenefit: LatticeBenefit = LatticeBenefit.None
private var cavBenefit: CavernBenefit = CavernBenefit.None
def LatticeLinkBenefit: LatticeBenefit = latBenefit
def LatticeLinkBenefit_=(bfit: LatticeBenefit): CaptureBenefit = {
latBenefit = bfit
LatticeLinkBenefit
}
def CavernLinkBenefit: CavernBenefit = cavBenefit
def CavernLinkBenefit_=(cfit: CavernBenefit): CavernBenefit = {
cavBenefit = cfit
CavernLinkBenefit
}
} }
class WarpGateDefinition(objectId: Int) extends BuildingDefinition(objectId) with SpawnPointDefinition class WarpGateDefinition(objectId: Int) extends BuildingDefinition(objectId) with SpawnPointDefinition

View file

@ -6,12 +6,9 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.{GlobalDefinitions, NtuContainer, SpawnPoint} import net.psforever.objects.{GlobalDefinitions, NtuContainer, SpawnPoint}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.packet.game.BuildingInfoUpdateMessage
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3} import net.psforever.types._
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.BuildingActor import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.definition.ObjectDefinition
import scala.collection.mutable
class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildingDefinition: WarpGateDefinition) class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildingDefinition: WarpGateDefinition)
extends Building(name, building_guid, map_id, zone, StructureType.WarpGate, buildingDefinition) extends Building(name, building_guid, map_id, zone, StructureType.WarpGate, buildingDefinition)
@ -21,31 +18,31 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
private var active: Boolean = true private var active: Boolean = true
/** what faction views this warp gate as a broadcast gate */ /** what faction views this warp gate as a broadcast gate */
private var broadcast: mutable.Set[PlanetSideEmpire.Value] = mutable.Set.empty[PlanetSideEmpire.Value] private var passageFor: Set[PlanetSideEmpire.Value] = Set(PlanetSideEmpire.NEUTRAL)
override def infoUpdateMessage(): BuildingInfoUpdateMessage = { override def infoUpdateMessage(): BuildingInfoUpdateMessage = {
BuildingInfoUpdateMessage( BuildingInfoUpdateMessage(
Zone.Number, Zone.Number,
MapId, MapId,
0, 0,
false, is_hacked = false,
PlanetSideEmpire.NEUTRAL, PlanetSideEmpire.NEUTRAL,
0L, 0L,
Faction, Faction, //should be neutral in most cases
0, //!! Field != 0 will cause malformed packet. See class def. 0, //Field != 0 will cause malformed packet. See class def.
None, None,
PlanetSideGeneratorState.Normal, PlanetSideGeneratorState.Normal,
true, //TODO? spawn_tubes_normal = true,
false, //force_dome_active force_dome_active = false,
0, //lattice_benefit Set(LatticeBenefit.None),
0, //cavern_benefit; !! Field > 0 will cause malformed packet. See class def. Set(CavernBenefit.None), //Field > 0 will cause malformed packet. See class def.
Nil, Nil,
0, 0,
false, unk6 = false,
8, //!! Field != 8 will cause malformed packet. See class def. 8, //!! Field != 8 will cause malformed packet. See class def.
None, None,
false, //boost_spawn_pain boost_spawn_pain = false,
false //boost_generator_pain boost_generator_pain = false
) )
} }
@ -72,54 +69,34 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
/** /**
* Determine whether any faction interacts with this warp gate as "broadcast." * Determine whether any faction interacts with this warp gate as "broadcast."
* The gate must be active first. * The gate must be active first.
* A broadcast gate allows specific factions only.
* @return `true`, if some faction sees this warp gate as a "broadcast gate"; * @return `true`, if some faction sees this warp gate as a "broadcast gate";
* `false`, otherwise * `false`, otherwise
*/ */
def Broadcast: Boolean = Active && broadcast.nonEmpty def Broadcast: Boolean = Active && !passageFor.contains(PlanetSideEmpire.NEUTRAL)
/** /**
* Determine whether a specific faction interacts with this warp gate as "broadcast." * Determine whether a specific faction interacts with this warp gate as "broadcast."
* The warp gate being `NEUTRAL` should allow for any polled faction to interact.
* The gate must be active first.
* @return `true`, if the given faction interacts with this warp gate as a "broadcast gate"; * @return `true`, if the given faction interacts with this warp gate as a "broadcast gate";
* `false`, otherwise * `false`, otherwise
*/ */
def Broadcast(faction: PlanetSideEmpire.Value): Boolean = { def Broadcast(faction: PlanetSideEmpire.Value): Boolean = {
Active && (broadcast.contains(faction) || broadcast.contains(PlanetSideEmpire.NEUTRAL)) Broadcast && passageFor.contains(faction)
} }
/** /**
* Toggle whether the warp gate's faction-affiliated force interacts with this warp gate as "broadcast." * Which factions interact with this warp gate as "broadcast"?
* Other "broadcast" associations are not affected.
* The gate must be active first.
* @param bcast `true`, if the faction-affiliated force interacts with this gate as broadcast;
* `false`, if not
* @return the set of all factions who interact with this warp gate as "broadcast" * @return the set of all factions who interact with this warp gate as "broadcast"
*/ */
def Broadcast_=(bcast: Boolean): Set[PlanetSideEmpire.Value] = { def AllowBroadcastFor: Set[PlanetSideEmpire.Value] = passageFor
if (Active) {
if (bcast) {
broadcast += Faction
} else {
broadcast -= Faction
}
}
broadcast.toSet
}
/**
* Which factions interact with this warp gate as "broadcast?"
* @return the set of all factions who interact with this warp gate as "broadcast"
*/
def BroadcastFor: Set[PlanetSideEmpire.Value] = broadcast.toSet
/** /**
* Allow a faction to interact with a given warp gate as "broadcast" if it is active. * Allow a faction to interact with a given warp gate as "broadcast" if it is active.
* @param bcast the faction * @param bcast the faction
* @return the set of all factions who interact with this warp gate as "broadcast" * @return the set of all factions who interact with this warp gate as "broadcast"
*/ */
def BroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = { def AllowBroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
(broadcast += bcast).toSet AllowBroadcastFor_=(Set(bcast))
} }
/** /**
@ -127,26 +104,14 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
* @param bcast the factions * @param bcast the factions
* @return the set of all factions who interact with this warp gate as "broadcast" * @return the set of all factions who interact with this warp gate as "broadcast"
*/ */
def BroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = { def AllowBroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
(broadcast ++= bcast).toSet val validFactions = bcast.filterNot(_ == PlanetSideEmpire.NEUTRAL)
} passageFor = if (bcast.isEmpty || validFactions.isEmpty) {
Set(PlanetSideEmpire.NEUTRAL)
/** } else {
* Disallow a faction to interact with a given warp gate as "broadcast." validFactions
* @param bcast the faction }
* @return the set of all factions who interact with this warp gate as "broadcast" AllowBroadcastFor
*/
def StopBroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
(broadcast -= bcast).toSet
}
/**
* Disallow some factions to interact with a given warp gate as "broadcast."
* @param bcast the factions
* @return the set of all factions who interact with this warp gate as "broadcast"
*/
def StopBroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
(broadcast --= bcast).toSet
} }
def Owner: PlanetSideServerObject = this def Owner: PlanetSideServerObject = this
@ -157,11 +122,13 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
def MaxNtuCapacitor : Float = Int.MaxValue def MaxNtuCapacitor : Float = Int.MaxValue
override def isOffline: Boolean = !Active
override def NtuSource: Option[NtuContainer] = Some(this) override def NtuSource: Option[NtuContainer] = Some(this)
override def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = false override def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = false
override def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = Set.empty override def latticeConnectedFacilityBenefits(): Set[LatticeBenefit] = Set.empty[LatticeBenefit]
override def Definition: WarpGateDefinition = buildingDefinition override def Definition: WarpGateDefinition = buildingDefinition
} }

View file

@ -2,17 +2,12 @@
package net.psforever.objects.serverobject.terminals package net.psforever.objects.serverobject.terminals
import akka.actor.{ActorContext, ActorRef} import akka.actor.{ActorContext, ActorRef}
import net.psforever.objects.avatar.Certification import net.psforever.objects.{Default, Player}
import net.psforever.objects.definition.ImplantDefinition
import net.psforever.objects.{Default, Player, Vehicle}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.loadouts.{InfantryLoadout, VehicleLoadout}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.packet.game.ItemTransactionMessage import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition._ import net.psforever.objects.serverobject.terminals.tabs.Tab
import net.psforever.types.{ExoSuitType, TransactionType} import net.psforever.types.TransactionType
import scala.collection.mutable import scala.collection.mutable
@ -37,8 +32,8 @@ import scala.collection.mutable
class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) { class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
/** An internal object organizing the different specification options found on a terminal's UI. */ /** An internal object organizing the different specification options found on a terminal's UI. */
private val tabs: mutable.HashMap[Int, OrderTerminalDefinition.Tab] = private val tabs: mutable.HashMap[Int, Tab] =
new mutable.HashMap[Int, OrderTerminalDefinition.Tab]() new mutable.HashMap[Int, Tab]()
/** Disconnect the ability to return stock back to the terminal /** Disconnect the ability to return stock back to the terminal
* from the type of stock available from the terminal in general * from the type of stock available from the terminal in general
@ -47,7 +42,7 @@ class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
*/ */
private var sellEquipmentDefault: Boolean = false private var sellEquipmentDefault: Boolean = false
def Tab: mutable.HashMap[Int, OrderTerminalDefinition.Tab] = tabs def Tab: mutable.HashMap[Int, Tab] = tabs
def SellEquipmentByDefault: Boolean = sellEquipmentDefault def SellEquipmentByDefault: Boolean = sellEquipmentDefault
@ -107,337 +102,6 @@ class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
} }
object OrderTerminalDefinition { object OrderTerminalDefinition {
/**
* A basic tab outlining the specific type of stock available from this part of the terminal's interface.
* @see `ItemTransactionMessage`
*/
sealed trait Tab {
def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange
def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = Terminal.NoDeal()
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit
}
/**
* The tab used to select an exo-suit to be worn by the player.
* @see `ExoSuitType`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
*/
final case class ArmorPage(stock: Map[String, (ExoSuitType.Value, Int)]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
Terminal.BuyExosuit(suit, subtype)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.player.Actor ! msg
}
}
/**
* An expanded form of the tab used to select an exo-suit to be worn by the player that also provides some equipment.
* @see `ExoSuitType`
* @see `Equipment`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
* @param items the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a curried function that produces an `Equipment` object
*/
final case class ArmorWithAmmoPage(stock: Map[String, (ExoSuitType.Value, Int)], items: Map[String, () => Equipment])
extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
Terminal.BuyExosuit(suit, subtype)
case _ =>
items.get(msg.item_name) match {
case Some(item) =>
Terminal.BuyEquipment(item())
case _ =>
Terminal.NoDeal()
}
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.response match {
case _: Terminal.BuyExosuit => msg.player.Actor ! msg
case _ => sender ! msg
}
}
}
/**
* The tab used to select a certification to be utilized by the player.
* Only certifications may be returned to the interface defined by this page.
*
* @see `CertificationType`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a `CertificationType` value
*/
final case class CertificationPage(stock: Seq[Certification]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.find(_.name == msg.item_name) match {
case Some(cert: Certification) =>
Terminal.LearnCertification(cert)
case _ =>
Terminal.NoDeal()
}
}
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.find(_.name == msg.item_name) match {
case Some(cert: Certification) =>
Terminal.SellCertification(cert)
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/**
* The tab used to produce an `Equipment` object to be used by the player.
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a curried function that produces an `Equipment` object
*/
final case class EquipmentPage(stock: Map[String, () => Equipment]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(item) =>
Terminal.BuyEquipment(item())
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/**
* The tab used to select an implant to be utilized by the player.
* A maximum of three implants can be obtained by any player at a time depending on the player's battle rank.
* Only implants may be returned to the interface defined by this page.
* @see `ImplantDefinition`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a `CertificationType` value
*/
final case class ImplantPage(stock: Map[String, ImplantDefinition]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(implant: ImplantDefinition) =>
Terminal.LearnImplant(implant)
case None =>
Terminal.NoDeal()
}
}
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(implant: ImplantDefinition) =>
Terminal.SellImplant(implant)
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/**
* The base class for "loadout" type tabs.
* Defines logic for enumerating items and entities that should be eliminated from being loaded.
* The method for filtering those excluded items, if applicable,
* and management of the resulting loadout object
* is the responsibility of the specific tab that is instantiated.
*/
abstract class LoadoutTab extends Tab {
private var contraband: Seq[Any] = Nil
def Exclude: Seq[Any] = contraband
def Exclude_=(equipment: Any): Seq[Any] = {
contraband = Seq(equipment)
Exclude
}
def Exclude_=(equipmentList: Seq[Any]): Seq[Any] = {
contraband = equipmentList
Exclude
}
}
/**
* The tab used to select which custom loadout the player is using.
* Player loadouts are defined by an exo-suit to be worn by the player
* and equipment in the holsters and the inventory.
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
* no extra information specific to this page is necessary.
* If an exo-suit type is considered excluded, the whole loadout is blocked.
* If the exclusion is written as a `Tuple` object `(A, B)`,
* `A` will be expected as an exo-suit type, and `B` will be expected as its subtype,
* and the pair must both match to block the whole loadout.
* If any of the player's inventory is considered excluded, only those items will be filtered.
* @see `ExoSuitType`
* @see `Equipment`
* @see `InfantryLoadout`
* @see `Loadout`
*/
//TODO block equipment by blocking ammunition type
final case class InfantryLoadoutPage() extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1) match {
case Some(loadout: InfantryLoadout)
if !Exclude.contains(loadout.exosuit) && !Exclude.contains((loadout.exosuit, loadout.subtype)) =>
val holsters = loadout.visible_slots
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
val inventory = loadout.inventory
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.player.Actor ! msg
}
}
/**
* The tab used to select which custom loadout the player's vehicle is using.
* Vehicle loadouts are defined by a (superfluous) redefinition of the vehicle's mounted weapons
* and equipment in the trunk.
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
* no extra information specific to this page is necessary.
* If a vehicle type (by definition) is considered excluded, the whole loadout is blocked.
* If any of the vehicle's inventory is considered excluded, only those items will be filtered.
* @see `Equipment`
* @see `Loadout`
* @see `VehicleLoadout`
*/
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + lineOffset) match {
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
val weapons = loadout.visible_slots
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
val inventory = loadout.inventory
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
Terminal.VehicleLoadout(loadout.vehicle_definition, weapons, inventory)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
val player = msg.player
player.Zone.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) => vehicle.Actor ! msg
case _ => sender ! Terminal.TerminalMessage(player, msg.msg, Terminal.NoDeal())
}
}
}
/**
* The tab used to select a vehicle to be spawned for the player.
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
* and equipment in the trunk
* for the purpose of establishing default contents.
* @see `Equipment`
* @see `Loadout`
* @see `Vehicle`
* @see `VehicleLoadout`
*/
import net.psforever.objects.loadouts.{Loadout => Contents} //distinguish from Terminal.Loadout message
final case class VehiclePage(stock: Map[String, () => Vehicle], trunk: Map[String, Contents]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(vehicle) =>
val (weapons, inventory) = trunk.get(msg.item_name) match {
case Some(loadout: VehicleLoadout) =>
(
loadout.visible_slots.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
}),
loadout.inventory.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
)
case _ =>
(List.empty, List.empty)
}
Terminal.BuyVehicle(vehicle(), weapons, inventory)
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/**
* The special page used by the `bfr_terminal` to select a vehicle to be spawned
* based on the player's previous loadouts for battleframe vehicles.
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
* and equipment in the trunk.
* @see `Equipment`
* @see `Loadout`
* @see `Vehicle`
* @see `VehicleLoadout`
*/
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + 15) match {
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
vehicles.get(loadout.vehicle_definition.Name) match {
case Some(vehicle) =>
val weapons = loadout.visible_slots.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
val inventory = loadout.inventory.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
Terminal.BuyVehicle(vehicle(), weapons, inventory)
case None =>
Terminal.NoDeal()
}
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/** /**
* Assemble some logic for a provided object. * Assemble some logic for a provided object.
* @param obj an `Amenity` object; * @param obj an `Amenity` object;

View file

@ -0,0 +1,29 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.types.ExoSuitType
/**
* The tab used to select an exo-suit to be worn by the player.
* @see `ExoSuitType`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
*/
final case class ArmorPage(stock: Map[String, (ExoSuitType.Value, Int)]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
Terminal.BuyExosuit(suit, subtype)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.player.Actor ! msg
}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.types.ExoSuitType
/**
* An expanded form of the tab used to select an exo-suit to be worn by the player that also provides some equipment.
* @see `ExoSuitType`
* @see `Equipment`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
* @param items the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a curried function that produces an `Equipment` object
*/
final case class ArmorWithAmmoPage(stock: Map[String, (ExoSuitType.Value, Int)], items: Map[String, () => Equipment])
extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
Terminal.BuyExosuit(suit, subtype)
case _ =>
items.get(msg.item_name) match {
case Some(item) =>
Terminal.BuyEquipment(item())
case _ =>
Terminal.NoDeal()
}
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.response match {
case _: Terminal.BuyExosuit => msg.player.Actor ! msg
case _ => sender ! msg
}
}
}

View file

@ -0,0 +1,47 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.VehicleLoadout
import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.serverobject.terminals.{EquipmentTerminalDefinition, Terminal}
import net.psforever.packet.game.ItemTransactionMessage
/**
* The special page used by the `bfr_terminal` to select a vehicle to be spawned
* based on the player's previous loadouts for battleframe vehicles.
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
* and equipment in the trunk.
* @see `Equipment`
* @see `Loadout`
* @see `Vehicle`
* @see `VehicleLoadout`
*/
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + 15) match {
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
vehicles.get(loadout.vehicle_definition.Name) match {
case Some(vehicle) =>
val weapons = loadout.visible_slots.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
val inventory = loadout.inventory.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.exists(_.checkRule(player, msg, entry.obj)) }
Terminal.BuyVehicle(vehicle(), weapons, inventory)
case None =>
Terminal.NoDeal()
}
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.avatar.Certification
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to select a certification to be utilized by the player.
* Only certifications may be returned to the interface defined by this page.
* @see `CertificationType`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a `CertificationType` value
*/
final case class CertificationPage(stock: Seq[Certification]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.find(_.name == msg.item_name) match {
case Some(cert: Certification) =>
Terminal.LearnCertification(cert)
case _ =>
Terminal.NoDeal()
}
}
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.find(_.name == msg.item_name) match {
case Some(cert: Certification) =>
Terminal.SellCertification(cert)
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to produce an `Equipment` object to be used by the player.
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a curried function that produces an `Equipment` object
*/
final case class EquipmentPage(stock: Map[String, () => Equipment]) extends ScrutinizedTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(item) =>
val createdItem = item()
if (!Exclude.exists(_.checkRule(player, msg, createdItem))) {
Terminal.BuyEquipment(createdItem)
} else {
Terminal.NoDeal()
}
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}

View file

@ -0,0 +1,130 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
import net.psforever.objects.definition.{EquipmentDefinition, VehicleDefinition}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.types.ExoSuitType
/**
* An allowance test to be utilized by tabs and pages of an order terminal.
* @see `ScrutinizedTab`
*/
trait ExclusionRule {
/**
* An allowance test to be utilized by tabs and pages of an order terminal.
* @param player the player
* @param msg the original order message from the client
* @param obj the produced item that is being tested
* @return `true`, if the item qualifies this test and should be excluded;
* `false` if the item did not pass the test and can be included
*/
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean
}
/**
* Do not allow the player to don certain exo-suits.
* @param illegalSuit the banned exo-suit type
* @param illegalSubtype the banned exo-suit subtype
*/
final case class NoExoSuitRule(illegalSuit: ExoSuitType.Value, illegalSubtype: Int = 0) extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case exosuit: ExoSuitType.Value => exosuit == illegalSuit
case (exosuit: ExoSuitType.Value, subtype: Int) => exosuit == illegalSuit && subtype == illegalSubtype
case _ => false
}
}
}
/**
* Do not allow the player to acquire certain equipment.
* @param illegalDefinition the definition entry for the specific type of equipment
*/
final case class NoEquipmentRule(illegalDefinition: EquipmentDefinition) extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case equipment: Equipment => equipment.Definition eq illegalDefinition
case _ => false
}
}
}
/**
* Do not allow cavern equipment.
*/
case object NoCavernEquipmentRule extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case equipment: Equipment => GlobalDefinitions.isCavernWeapon(equipment.Definition)
case _ => false
}
}
}
/**
* Do not allow the player to spawn cavern equipment if not pulled from a facility and
* only if the facility is subject to the benefit of an appropriate cavern perk.
*/
case object CavernEquipmentQuestion extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case equipment: Equipment =>
import net.psforever.objects.serverobject.structures.Building
if(GlobalDefinitions.isCavernWeapon(equipment.Definition)) {
(player.Zone.GUID(msg.terminal_guid) match {
case Some(term: Amenity) => Some(term.Owner)
case _ => None
}) match {
case Some(b: Building) => b.connectedCavern().isEmpty
case _ => true
}
} else {
false
}
case _ =>
false
}
}
}
/**
* Do not allow the player to acquire certain vehicles.
* @param illegalDefinition the definition entry for the specific type of vehicle
*/
final case class NoVehicleRule(illegalDefinition: VehicleDefinition) extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case vehicleDef: VehicleDefinition => vehicleDef eq illegalDefinition
case _ => false
}
}
}
/**
* Do not allow the player to spawn cavern vehicles if not pulled from a facility and
* only if the facility is subject to the benefit of an appropriate cavern perk.
*/
case object CavernVehicleQuestion extends ExclusionRule {
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
obj match {
case vehicle: Vehicle =>
import net.psforever.objects.serverobject.structures.Building
if(GlobalDefinitions.isCavernVehicle(vehicle.Definition)) {
(player.Zone.GUID(msg.terminal_guid) match {
case Some(term: Amenity) => Some(term.Owner)
case _ => None
}) match {
case Some(b: Building) => b.connectedCavern().isEmpty
case _ => true
}
} else {
false
}
case _ =>
false
}
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.definition.ImplantDefinition
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to select an implant to be utilized by the player.
* A maximum of three implants can be obtained by any player at a time depending on the player's battle rank.
* Only implants may be returned to the interface defined by this page.
* @see `ImplantDefinition`
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
* the value is a `CertificationType` value
*/
final case class ImplantPage(stock: Map[String, ImplantDefinition]) extends Tab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(implant: ImplantDefinition) =>
Terminal.LearnImplant(implant)
case None =>
Terminal.NoDeal()
}
}
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(implant: ImplantDefinition) =>
Terminal.SellImplant(implant)
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition.BuildSimplifiedPattern
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to select which custom loadout the player is using.
* Player loadouts are defined by an exo-suit to be worn by the player
* and equipment in the holsters and the inventory.
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
* no extra information specific to this page is necessary.
* If an exo-suit type is considered excluded, the whole loadout is blocked.
* If the exclusion is written as a `Tuple` object `(A, B)`,
* `A` will be expected as an exo-suit type, and `B` will be expected as its subtype,
* and the pair must both match to block the whole loadout.
* If any of the player's inventory is considered excluded, only those items will be filtered.
* @see `ExoSuitType`
* @see `Equipment`
* @see `InfantryLoadout`
* @see `Loadout`
*/
final case class InfantryLoadoutPage() extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1) match {
case Some(loadout: InfantryLoadout)
if !Exclude.exists(_.checkRule(player, msg, loadout.exosuit)) &&
!Exclude.exists(_.checkRule(player, msg, (loadout.exosuit, loadout.subtype))) =>
val holsters = loadout.visible_slots
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.exists(_.checkRule(player, msg, entry.obj)) }
val inventory = loadout.inventory
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.exists(_.checkRule(player, msg, entry.obj)) }
Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
msg.player.Actor ! msg
}
}

View file

@ -0,0 +1,10 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
/**
* The base class for "loadout" type tabs.
* The method for filtering those excluded items, if applicable,
* and management of the resulting loadout object
* is the responsibility of the specific tab that is instantiated.
*/
abstract class LoadoutTab extends ScrutinizedTab

View file

@ -0,0 +1,23 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
/**
* A basic tab outlining the specific type of stock available from this part of the terminal's interface.
* Defines logic for enumerating items and entities that should be eliminated from being loaded.
* @see `ItemTransactionMessage`
*/
trait ScrutinizedTab extends Tab {
private var contraband: Seq[ExclusionRule] = Nil
def Exclude: Seq[ExclusionRule] = contraband
def Exclude_=(equipment: ExclusionRule): Seq[ExclusionRule] = {
contraband = Seq(equipment)
Exclude
}
def Exclude_=(equipmentList: Seq[ExclusionRule]): Seq[ExclusionRule] = {
contraband = equipmentList
Exclude
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.Player
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* A basic tab outlining the specific type of stock available from this part of the terminal's interface.
* @see `ItemTransactionMessage`
*/
trait Tab {
def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange
def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = Terminal.NoDeal()
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.VehicleLoadout
import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition.BuildSimplifiedPattern
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to select which custom loadout the player's vehicle is using.
* Vehicle loadouts are defined by a (superfluous) redefinition of the vehicle's mounted weapons
* and equipment in the trunk.
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
* no extra information specific to this page is necessary.
* If a vehicle type (by definition) is considered excluded, the whole loadout is blocked.
* If any of the vehicle's inventory is considered excluded, only those items will be filtered.
* @see `Equipment`
* @see `Loadout`
* @see `VehicleLoadout`
*/
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + lineOffset) match {
case Some(loadout: VehicleLoadout) =>
val weapons = loadout.visible_slots
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
val inventory = loadout.inventory
.map(entry => {
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot { entry => Exclude.exists(_.checkRule(player, msg, entry.obj)) }
Terminal.VehicleLoadout(loadout.vehicle_definition, weapons, inventory)
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
val player = msg.player
player.Zone.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) => vehicle.Actor ! msg
case _ => sender ! Terminal.TerminalMessage(player, msg.msg, Terminal.NoDeal())
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.serverobject.terminals.tabs
import akka.actor.ActorRef
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.VehicleLoadout
import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.serverobject.terminals.{EquipmentTerminalDefinition, Terminal}
import net.psforever.packet.game.ItemTransactionMessage
/**
* The tab used to select a vehicle to be spawned for the player.
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
* and equipment in the trunk
* for the purpose of establishing default contents.
* @see `Equipment`
* @see `Loadout`
* @see `Vehicle`
* @see `VehicleLoadout`
*/
import net.psforever.objects.loadouts.{Loadout => Contents} //distinguish from Terminal.Loadout message
final case class VehiclePage(stock: Map[String, () => Vehicle], trunk: Map[String, Contents]) extends ScrutinizedTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
stock.get(msg.item_name) match {
case Some(vehicle) =>
val createdVehicle = vehicle()
if(!Exclude.exists(_.checkRule(player, msg, createdVehicle))) {
val (weapons, inventory) = trunk.get(msg.item_name) match {
case Some(loadout: VehicleLoadout) =>
(
loadout.visible_slots.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
}),
loadout.inventory.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
.filterNot( item => Exclude.exists(_.checkRule(player, msg, item.obj)))
)
case _ =>
(List.empty, List.empty)
}
Terminal.BuyVehicle(createdVehicle, weapons, inventory)
} else {
Terminal.NoDeal()
}
case None =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}

View file

@ -253,7 +253,9 @@ case object SpikerChargeDamage extends ProjectileDamageModifiers.Mod {
(projectile.fire_mode, projectile.profile.Charging) match { (projectile.fire_mode, projectile.profile.Charging) match {
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) => case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f)) val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f))
cause.damageModel.DamageUsing(info.min) + (damage * chargeQuality).toInt val min = cause.damageModel.DamageUsing(info.min)
val range = if (damage > min) { damage - min } else { min - damage }
min + (range * chargeQuality).toInt
case _ => case _ =>
damage damage
} }

View file

@ -33,6 +33,7 @@ import scala.util.Try
import akka.actor.typed import akka.actor.typed
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.actors.zone.building.WarpGateLogic
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.guid.pool.NumberPool import net.psforever.objects.guid.pool.NumberPool
@ -51,6 +52,9 @@ import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.blockmap.BlockMap import net.psforever.objects.zones.blockmap.BlockMap
import net.psforever.services.Service import net.psforever.services.Service
import scala.annotation.tailrec
import scala.concurrent.{Future, Promise}
/** /**
* A server object representing the one-landmass planets as well as the individual subterranean caverns.<br> * A server object representing the one-landmass planets as well as the individual subterranean caverns.<br>
* <br> * <br>
@ -168,6 +172,18 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/ */
private var vehicleEvents: ActorRef = Default.Actor private var vehicleEvents: ActorRef = Default.Actor
/**
* When the zone has completed initializing, fulfill this promise.
* @see `init(ActorContext)`
*/
private var zoneInitialized: Promise[Boolean] = Promise[Boolean]()
/**
* When the zone has completed initializing, this will be the future.
* @see `init(ActorContext)`
*/
def ZoneInitialized(): Future[Boolean] = zoneInitialized.future
/** /**
* Establish the basic accessible conditions necessary for a functional `Zone`.<br> * Establish the basic accessible conditions necessary for a functional `Zone`.<br>
* <br> * <br>
@ -213,8 +229,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
AssignAmenities() AssignAmenities()
CreateSpawnGroups() CreateSpawnGroups()
PopulateBlockMap() PopulateBlockMap()
validate() validate()
zoneInitialized.success(true)
} }
} }
@ -358,7 +375,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
case (building, _) => case (building, _) =>
building match { building match {
case warpGate: WarpGate => case warpGate: WarpGate =>
warpGate.Faction == faction || warpGate.Faction == PlanetSideEmpire.NEUTRAL || warpGate.Broadcast warpGate.Faction == faction || warpGate.Broadcast(faction)
case _ => case _ =>
building.Faction == faction building.Faction == faction
} }
@ -570,6 +587,20 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
lattice lattice
} }
def AddIntercontinentalLatticeLink(bldgA: Building, bldgB: Building): Graph[Building, UnDiEdge] = {
if ((this eq bldgA.Zone) && (bldgA.Zone ne bldgB.Zone)) {
lattice ++= Set(bldgA ~ bldgB)
}
Lattice
}
def RemoveIntercontinentalLatticeLink(bldgA: Building, bldgB: Building): Graph[Building, UnDiEdge] = {
if ((this eq bldgA.Zone) && (bldgA.Zone ne bldgB.Zone)) {
lattice --= Set(bldgA ~ bldgB)
}
Lattice
}
def zipLinePaths: List[ZipLinePath] = { def zipLinePaths: List[ZipLinePath] = {
map.zipLinePaths map.zipLinePaths
} }
@ -674,15 +705,19 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
} }
private def MakeLattice(): Unit = { private def MakeLattice(): Unit = {
lattice ++= map.latticeLink.map { lattice ++= map.latticeLink
case (source, target) => .filterNot {
val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match { case (a, _) => a.contains("/") //ignore intercontinental lattice connections
case (Some(sBuilding), Some(tBuilding)) => (sBuilding, tBuilding) }
case _ => .map {
throw new NoSuchElementException(s"Can't create lattice link between $source $target. Source is missing") case (source, target) =>
} val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match {
sourceBuilding ~ targetBuilding case (Some(sBuilding), Some(tBuilding)) => (sBuilding, tBuilding)
} case _ =>
throw new NoSuchElementException(s"Zone $id - can't create lattice link between $source and $target.")
}
sourceBuilding ~ targetBuilding
}
} }
private def CreateSpawnGroups(): Unit = { private def CreateSpawnGroups(): Unit = {
@ -1109,6 +1144,105 @@ object Zone {
} }
} }
/**
* Starting from an overworld zone facility,
* find a lattice connected cavern facility that is the same faction as this starting building.
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
* do not let the search escape the current zone into another.
* If we start in a cavern zone, do not continue a fruitless search;
* just fail.
* @return the discovered faction-aligned cavern facility
*/
def findConnectedCavernFacility(building: Building): Option[Building] = {
if (building.Zone.map.cavern) {
None
} else {
val neighbors = building.AllNeighbours.getOrElse(Set.empty[Building]).toList
recursiveFindConnectedCavernFacility(building.Faction, neighbors.headOption, neighbors.drop(1), Set(building.MapId))
}
}
/**
* Starting from an overworld zone facility,
* find a lattice connected cavern facility that is the same faction as this starting building.
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
* do not let the search escape the current zone into another.
* @param currBuilding the proposed current facility to check
* @param nextNeighbors the facilities that are yet to be searched
* @param visitedNeighbors the facilities that have been searched already
* @return the discovered faction-aligned cavern facility
*/
@tailrec
private def recursiveFindConnectedCavernFacility(
sampleFaction: PlanetSideEmpire.Value,
currBuilding: Option[Building],
nextNeighbors: List[Building],
visitedNeighbors: Set[Int]
): Option[Building] = {
if(currBuilding.isEmpty) {
None
} else {
val building = currBuilding.head
if (!visitedNeighbors.contains(building.MapId)
&& (building match {
case wg: WarpGate => wg.Faction == sampleFaction || wg.Broadcast(sampleFaction)
case _ => building.Faction == sampleFaction
})
&& !building.CaptureTerminalIsHacked
&& building.NtuLevel > 0
&& (building.Generator match {
case Some(o) => o.Condition != PlanetSideGeneratorState.Destroyed
case _ => true
})
) {
(building match {
case wg: WarpGate => traverseWarpGateInSearchOfOwnedCavernFaciity(sampleFaction, wg)
case _ => None
}) match {
case out @ Some(_) =>
out
case _ =>
val newVisitedNeighbors = visitedNeighbors ++ Set(building.MapId)
val newNeighbors = nextNeighbors ++ building.AllNeighbours
.getOrElse(Set.empty[Building])
.toList
.filterNot { b => newVisitedNeighbors.contains(b.MapId) }
recursiveFindConnectedCavernFacility(
sampleFaction,
newNeighbors.headOption,
newNeighbors.drop(1),
newVisitedNeighbors
)
}
} else {
recursiveFindConnectedCavernFacility(
sampleFaction,
nextNeighbors.headOption,
nextNeighbors.drop(1),
visitedNeighbors ++ Set(building.MapId)
)
}
}
}
/**
* Trace the extended neighborhood of the warp gate to a cavern facility that has the same faction affinity.
* @param faction the faction that all connected factions must have affinity with
* @param target the warp gate from which to conduct a local search
* @return if discovered, the first faction affiliated facility in a connected cavern
*/
private def traverseWarpGateInSearchOfOwnedCavernFaciity(
faction: PlanetSideEmpire.Value,
target: WarpGate
): Option[Building] = {
WarpGateLogic.findNeighborhoodWarpGate(target.Neighbours.getOrElse(Nil)) match {
case Some(gate) if gate.Zone.map.cavern =>
WarpGateLogic.findNeighborhoodNormalBuilding(gate.Neighbours(faction).getOrElse(Nil))
case _ =>
None
}
}
/** /**
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
* and informs those entities that they have affected by the aforementioned damage. * and informs those entities that they have affected by the aforementioned damage.

View file

@ -5,6 +5,7 @@ import akka.actor.{Actor, ActorRef, Props}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.{CorpseControl, PlayerControl} import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
import net.psforever.objects.{Default, Player} import net.psforever.objects.{Default, Player}
import net.psforever.types.Vector3
import scala.collection.concurrent.TrieMap import scala.collection.concurrent.TrieMap
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
@ -37,6 +38,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
tplayer.Zone = Zone.Nowhere tplayer.Zone = Zone.Nowhere
PlayerLeave(tplayer) PlayerLeave(tplayer)
if (tplayer.VehicleSeated.isEmpty) { if (tplayer.VehicleSeated.isEmpty) {
tplayer.Position = Vector3.Zero
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer) zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
} }
sender() ! Zone.Population.PlayerHasLeft(zone, player) sender() ! Zone.Population.PlayerHasLeft(zone, player)
@ -70,6 +72,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
case Some(tplayer) => case Some(tplayer) =>
PlayerLeave(tplayer) PlayerLeave(tplayer)
if (tplayer.VehicleSeated.isEmpty) { if (tplayer.VehicleSeated.isEmpty) {
tplayer.Position = Vector3.Zero
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer) zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
} }
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer)) sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))

View file

@ -4,6 +4,7 @@ package net.psforever.objects.zones
import akka.actor.Actor import akka.actor.Actor
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.{Default, Vehicle} import net.psforever.objects.{Default, Vehicle}
import net.psforever.types.Vector3
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
@ -52,6 +53,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
case Some(index) => case Some(index) =>
vehicleList.remove(index) vehicleList.remove(index)
vehicle.Definition.Uninitialize(vehicle, context) vehicle.Definition.Uninitialize(vehicle, context)
vehicle.Position = Vector3.Zero
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle) zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle) sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
case None => ; case None => ;

View file

@ -77,7 +77,12 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
* @return a conglomerate sector which lists all of the entities in the discovered sector(s) * @return a conglomerate sector which lists all of the entities in the discovered sector(s)
*/ */
def sector(p: Vector3, range: Float): SectorPopulation = { def sector(p: Vector3, range: Float): SectorPopulation = {
BlockMap.quickToSectorGroup(range, BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } ) val indices = BlockMap.findSectorIndices(blockMap = this, p, range)
if (indices.max < blocks.size) {
BlockMap.quickToSectorGroup(range, indices.map { blocks } )
} else {
SectorGroup(Nil)
}
} }
/** /**

View file

@ -556,7 +556,7 @@ object GamePacketOpcode extends Enumeration {
case 0xd3 => game.ComponentDamageMessage.decode case 0xd3 => game.ComponentDamageMessage.decode
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
case 0xd5 => game.PropertyOverrideMessage.decode case 0xd5 => game.PropertyOverrideMessage.decode
case 0xd6 => noDecoder(WarpgateLinkOverrideMessage) case 0xd6 => game.WarpgateLinkOverrideMessage.decode
case 0xd7 => noDecoder(EmpireBenefitsMessage) case 0xd7 => noDecoder(EmpireBenefitsMessage)
// 0xd8 // 0xd8
case 0xd8 => noDecoder(ForceEmpireMessage) case 0xd8 => noDecoder(ForceEmpireMessage)

View file

@ -2,6 +2,7 @@
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.PlanetSideEmpire
import scodec.Codec import scodec.Codec
import scodec.codecs._ import scodec.codecs._
@ -21,14 +22,40 @@ import scodec.codecs._
* @param nc players belonging to the New Conglomerate interact with this warp gate as a "broadcast gate" * @param nc players belonging to the New Conglomerate interact with this warp gate as a "broadcast gate"
* @param vs players belonging to the Vanu Sovereignty interact with this warp gate as a "broadcast gate" * @param vs players belonging to the Vanu Sovereignty interact with this warp gate as a "broadcast gate"
*/ */
final case class BroadcastWarpgateUpdateMessage(zone_id: Int, building_id: Int, tr: Boolean, nc: Boolean, vs: Boolean) final case class BroadcastWarpgateUpdateMessage(
extends PlanetSideGamePacket { zone_id: Int,
building_id: Int,
tr: Boolean,
nc: Boolean,
vs: Boolean
) extends PlanetSideGamePacket {
type Packet = BroadcastWarpgateUpdateMessage type Packet = BroadcastWarpgateUpdateMessage
def opcode = GamePacketOpcode.BroadcastWarpgateUpdateMessage def opcode = GamePacketOpcode.BroadcastWarpgateUpdateMessage
def encode = BroadcastWarpgateUpdateMessage.encode(this) def encode = BroadcastWarpgateUpdateMessage.encode(this)
} }
object BroadcastWarpgateUpdateMessage extends Marshallable[BroadcastWarpgateUpdateMessage] { object BroadcastWarpgateUpdateMessage extends Marshallable[BroadcastWarpgateUpdateMessage] {
def apply(
zoneId: Int,
buildingId: Int,
faction: PlanetSideEmpire.Value
): BroadcastWarpgateUpdateMessage = {
BroadcastWarpgateUpdateMessage(zoneId, buildingId, Set(faction))
}
def apply(
zoneId: Int,
buildingId: Int,
factions: Set[PlanetSideEmpire.Value]
): BroadcastWarpgateUpdateMessage = {
val f = {
val out = Array.fill(PlanetSideEmpire.values.size)(false)
factions.map(_.id).foreach { i => out.update(i, true) }
out
}
BroadcastWarpgateUpdateMessage(zoneId, buildingId, f(0), f(1), f(2))
}
implicit val codec: Codec[BroadcastWarpgateUpdateMessage] = ( implicit val codec: Codec[BroadcastWarpgateUpdateMessage] = (
("zone_id" | uint16L) :: ("zone_id" | uint16L) ::
("building_id" | uint16L) :: ("building_id" | uint16L) ::

View file

@ -1,8 +1,9 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.packet.game package net.psforever.packet.game
import enumeratum.values.IntEnum
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState} import net.psforever.types._
import scodec.{Attempt, Codec, Err} import scodec.{Attempt, Codec, Err}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
@ -47,28 +48,7 @@ final case class Additional3(unk1: Boolean, unk2: Int)
* A parameter that is not applicable for a given asset, e.g., NTU for a field tower, will be ignored. * A parameter that is not applicable for a given asset, e.g., NTU for a field tower, will be ignored.
* A collision between some parameters can occur. * A collision between some parameters can occur.
* For example, if `is_hacking` is `false`, the other hacking fields are considered invalid. * For example, if `is_hacking` is `false`, the other hacking fields are considered invalid.
* If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.<br> * If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.
* <br>
* Lattice benefits: (stackable)<br>
* `
* 00 - None<br>
* 01 - Amp Station<br>
* 02 - Dropship Center<br>
* 04 - Bio Laboratory<br>
* 08 - Interlink Facility<br>
* 16 - Technology Plant<br>
* `
* <br>
* Cavern benefits: (stackable)<br>
* `
* 000 - None<br>
* 004 - Speed Module<br>
* 008 - Shield Module<br>
* 016 - Vehicle Module<br>
* 032 - Equipment Module<br>
* 064 - Health Module<br>
* 128 - Pain Module<br>
* `
* @param continent_id the continent (zone) * @param continent_id the continent (zone)
* @param building_map_id the map id of this building from the MPO files * @param building_map_id the map id of this building from the MPO files
* @param ntu_level if the building has a silo, the amount of NTU in that silo; * @param ntu_level if the building has a silo, the amount of NTU in that silo;
@ -87,9 +67,9 @@ final case class Additional3(unk1: Boolean, unk2: Int)
* @param force_dome_active if the building is a capitol facility, whether the force dome is active * @param force_dome_active if the building is a capitol facility, whether the force dome is active
* @param lattice_benefit the benefits from other Lattice-linked bases does this building possess * @param lattice_benefit the benefits from other Lattice-linked bases does this building possess
* @param cavern_benefit cavern benefits; * @param cavern_benefit cavern benefits;
* any non-zero value will cause the cavern module icon (yellow) to appear; * any non-zero value will cause the cavern module icon (yellow) to appear;
* proper module values cause the cavern module icon to render green; * proper module values cause the cavern module icon to render green;
* all benefits will report as due to a "Cavern Lock" * all benefits will report as due to a "Cavern Lock"
* @param unk4 na * @param unk4 na
* @param unk5 na * @param unk5 na
* @param unk6 na * @param unk6 na
@ -112,8 +92,8 @@ final case class BuildingInfoUpdateMessage(
generator_state: PlanetSideGeneratorState.Value, generator_state: PlanetSideGeneratorState.Value,
spawn_tubes_normal: Boolean, spawn_tubes_normal: Boolean,
force_dome_active: Boolean, force_dome_active: Boolean,
lattice_benefit: Int, lattice_benefit: Set[LatticeBenefit],
cavern_benefit: Int, cavern_benefit: Set[CavernBenefit],
unk4: List[Additional2], unk4: List[Additional2],
unk5: Long, unk5: Long,
unk6: Boolean, unk6: Boolean,
@ -128,7 +108,6 @@ final case class BuildingInfoUpdateMessage(
} }
object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] { object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] {
/** /**
* A `Codec` for a set of additional fields. * A `Codec` for a set of additional fields.
*/ */
@ -154,6 +133,45 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
("unk2" | uint2L) ("unk2" | uint2L)
).as[Additional3] ).as[Additional3]
/**
* A `Codec` for the benefits tallies
* transforming between numeric value and a set of enum values.
* The type of benefit is capable of being passed into the function.
* @param bits number of bits for this field
* @param objClass the benefits enumeration
* @tparam T the type of benefit in the enumeration (passive type)
* @return a `Codec` for the benefits tallies
*/
private def benefitCodecFunc[T <: CaptureBenefit](bits: Int, objClass: IntEnum[T]): Codec[Set[T]] = {
assert(
math.pow(2, bits) >= objClass.values.maxBy(data => data.value).value,
s"BuildingInfoUpdateMessage - $bits is not enough bits to represent ${objClass.getClass().getSimpleName()}"
)
uintL(bits).xmap[Set[T]](
{
case 0 =>
Set(objClass.values.find(_.value == 0).get)
case n =>
val values = objClass
.values
.sortBy(_.value)(Ordering.Int.reverse)
.dropRight(1) //drop value == 0
var curr = n
values
.collect {
case benefit if benefit.value <= curr =>
curr = curr - benefit.value
benefit
}.toSet
},
benefits => benefits.foldLeft[Int](0)(_ + _.value)
)
}
private val latticeBenefitCodec: Codec[Set[LatticeBenefit]] = benefitCodecFunc(bits = 5, LatticeBenefit)
private val cavernBenefitCodec: Codec[Set[CavernBenefit]] = benefitCodecFunc(bits = 10, CavernBenefit)
implicit val codec: Codec[BuildingInfoUpdateMessage] = ( implicit val codec: Codec[BuildingInfoUpdateMessage] = (
("continent_id" | uint16L) :: ("continent_id" | uint16L) ::
("building_id" | uint16L) :: ("building_id" | uint16L) ::
@ -163,17 +181,17 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
("hack_time_remaining" | uint32L) :: ("hack_time_remaining" | uint32L) ::
("empire_own" | PlanetSideEmpire.codec) :: ("empire_own" | PlanetSideEmpire.codec) ::
(("unk1" | uint32L) >>:~ { unk1 => (("unk1" | uint32L) >>:~ { unk1 =>
conditional(unk1 != 0L, "unk1x" | additional1_codec) :: conditional(unk1 != 0L, codec = "unk1x" | additional1_codec) ::
("generator_state" | PlanetSideGeneratorState.codec) :: ("generator_state" | PlanetSideGeneratorState.codec) ::
("spawn_tubes_normal" | bool) :: ("spawn_tubes_normal" | bool) ::
("force_dome_active" | bool) :: ("force_dome_active" | bool) ::
("lattice_benefit" | uintL(5)) :: ("lattice_benefit" | latticeBenefitCodec) ::
("cavern_benefit" | uintL(10)) :: ("cavern_benefit" | cavernBenefitCodec) ::
("unk4" | listOfN(uint4L, additional2_codec)) :: ("unk4" | listOfN(uint4L, additional2_codec)) ::
("unk5" | uint32L) :: ("unk5" | uint32L) ::
("unk6" | bool) :: ("unk6" | bool) ::
(("unk7" | uint4L) >>:~ { unk7 => (("unk7" | uint4L) >>:~ { unk7 =>
conditional(unk7 != 8, "unk7x" | additional3_codec) :: conditional(unk7 != 8, codec = "unk7x" | additional3_codec) ::
("boost_spawn_pain" | bool) :: ("boost_spawn_pain" | bool) ::
("boost_generator_pain" | bool) ("boost_generator_pain" | bool)
}) })

View file

@ -40,6 +40,9 @@ final case class ChatMsg(
} }
object ChatMsg extends Marshallable[ChatMsg] { object ChatMsg extends Marshallable[ChatMsg] {
def apply(messageType: ChatMessageType, contents: String): ChatMsg =
ChatMsg(messageType, wideContents=false, recipient="", contents, note=None)
implicit val codec: Codec[ChatMsg] = (("messagetype" | ChatMessageType.codec) >>:~ { messagetype_value => implicit val codec: Codec[ChatMsg] = (("messagetype" | ChatMessageType.codec) >>:~ { messagetype_value =>
(("has_wide_contents" | bool) >>:~ { isWide => (("has_wide_contents" | bool) >>:~ { isWide =>
("recipient" | PacketHelpers.encodedWideStringAligned(7)) :: ("recipient" | PacketHelpers.encodedWideStringAligned(7)) ::

View file

@ -0,0 +1,32 @@
// Copyright (c) 2022 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
final case class LinkOverride(
unk1: Int,
unk2: Int,
unk3: Int,
unk4: Int
)
final case class WarpgateLinkOverrideMessage(links: List[LinkOverride])
extends PlanetSideGamePacket {
type Packet = WarpgateLinkOverrideMessage
def opcode = GamePacketOpcode.WarpgateLinkOverrideMessage
def encode = WarpgateLinkOverrideMessage.encode(this)
}
object WarpgateLinkOverrideMessage extends Marshallable[WarpgateLinkOverrideMessage] {
private val linkOverrideCodec: Codec[LinkOverride] = (
("unk1" | uint16L) ::
("unk2" | uint16L) ::
("unk3" | uint16L) ::
("unk4" | uint16L)
).as[LinkOverride]
implicit val codec: Codec[WarpgateLinkOverrideMessage] =
("links" | listOfN(uint16L, linkOverrideCodec)).as[WarpgateLinkOverrideMessage]
}

View file

@ -316,6 +316,7 @@ object ObjectClass {
final val portable_manned_turret_vs = 688 final val portable_manned_turret_vs = 688
//projectiles //projectiles
final val aphelion_plasma_cloud = 96 final val aphelion_plasma_cloud = 96
final val aphelion_starfire_projectile = 108
final val flamethrower_fire_cloud = 301 final val flamethrower_fire_cloud = 301
final val hunter_seeker_missile_projectile = 405 //phoenix projectile final val hunter_seeker_missile_projectile = 405 //phoenix projectile
final val maelstrom_grenade_damager = 464 final val maelstrom_grenade_damager = 464
@ -327,6 +328,7 @@ object ObjectClass {
final val meteor_projectile_medium = 548 final val meteor_projectile_medium = 548
final val meteor_projectile_small = 549 final val meteor_projectile_small = 549
final val peregrine_particle_cannon_radiation_cloud = 655 final val peregrine_particle_cannon_radiation_cloud = 655
final val peregrine_sparrow_projectile = 661
final val phoenix_missile_guided_projectile = 675 //decimator projectile final val phoenix_missile_guided_projectile = 675 //decimator projectile
final val oicw_little_buddy = 601 //scorpion projectile's projectiles final val oicw_little_buddy = 601 //scorpion projectile's projectiles
final val oicw_projectile = 602 //scorpion projectile final val oicw_projectile = 602 //scorpion projectile
@ -1231,6 +1233,7 @@ object ObjectClass {
case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable") case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable")
//projectiles //projectiles
case ObjectClass.aphelion_plasma_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud") case ObjectClass.aphelion_plasma_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
case ObjectClass.aphelion_starfire_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor") case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor")
case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor") case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor")
@ -1240,6 +1243,7 @@ object ObjectClass {
case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor") case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor")
case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor") case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor")
case ObjectClass.peregrine_particle_cannon_radiation_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud") case ObjectClass.peregrine_particle_cannon_radiation_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
case ObjectClass.peregrine_sparrow_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
case ObjectClass.oicw_little_buddy => ConstructorData(LittleBuddyProjectileData.codec, "projectile") case ObjectClass.oicw_little_buddy => ConstructorData(LittleBuddyProjectileData.codec, "projectile")
case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")

View file

@ -12,10 +12,12 @@ object RemoteProjectiles {
final case object Meteor extends Data(0, 32) final case object Meteor extends Data(0, 32)
final case object Wasp extends Data(0, 208) final case object Wasp extends Data(0, 208)
final case object Sparrow extends Data(13107, 187) final case object Sparrow extends Data(13107, 187)
final case object PeregrineSparrow extends Data(13107, 187)
final case object OICW extends Data(13107, 195) final case object OICW extends Data(13107, 195)
final case object Striker extends Data(26214, 134) final case object Striker extends Data(26214, 134)
final case object HunterSeeker extends Data(39577, 201) final case object HunterSeeker extends Data(39577, 201)
final case object Starfire extends Data(39577, 249) final case object Starfire extends Data(39577, 249)
final case object AphelionStarfire extends Data(39577, 249)
//the oicw_little_buddy is handled by its own transcoder //the oicw_little_buddy is handled by its own transcoder
} }

View file

@ -0,0 +1,746 @@
// 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
lockedZones.foreach { monitor =>
sendToSession ! GalaxyServiceResponse(
"",
GalaxyResponse.LockedZoneUpdate(monitor.zone, math.max(0, monitor.start + monitor.duration - curr))
)
}
unlockedZones.foreach { monitor =>
sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone))
}
}
/**
* 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()
zoneRotationFunc(galaxyService)
lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes)
retimeZonesUponForcedRotation(galaxyService)
} 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}")
))
galaxyService ! GalaxyServiceMessage(GalaxyAction.UnlockedZoneUpdate(unlockingZone))
//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)
)
}
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(locking.zone, fullHoursBetweenRotationsAsMillis))
}
/**
* 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
if (zone.locked) {
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(zone.zone, newStart + fullDurationAsMillis - curr))
}
}
//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
if (zone.locked) {
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(zone.zone, zone.start + zone.duration - curr))
}
}
//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)
}
}

View file

@ -6,7 +6,7 @@ import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.{Player, SpawnPoint, Vehicle} import net.psforever.objects.{Player, SpawnPoint, Vehicle}
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.{Building, WarpGate}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.DroppodError import net.psforever.packet.game.DroppodError
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
@ -14,7 +14,8 @@ import net.psforever.util.Config
import net.psforever.zones.Zones import net.psforever.zones.Zones
import scala.collection.mutable import scala.collection.mutable
import scala.util.Random import scala.concurrent.Future
import scala.util.{Random, Success}
object InterstellarClusterService { object InterstellarClusterService {
val InterstellarClusterServiceKey: ServiceKey[Command] = val InterstellarClusterServiceKey: ServiceKey[Command] =
@ -51,6 +52,8 @@ object InterstellarClusterService {
zoneNumber: Int, zoneNumber: Int,
player: Player, player: Player,
target: PlanetSideGUID, target: PlanetSideGUID,
fromZoneNumber: Int,
fromGateGuid: PlanetSideGUID,
replyTo: ActorRef[SpawnPointResponse] replyTo: ActorRef[SpawnPointResponse]
) extends Command ) extends Command
@ -81,27 +84,50 @@ object InterstellarClusterService {
replyTo: ActorRef[DroppodLaunchExchange] replyTo: ActorRef[DroppodLaunchExchange]
) extends Command ) extends Command
final case class CavernRotation(msg: CavernRotationService.Command) extends Command
trait DroppodLaunchExchange trait DroppodLaunchExchange
final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange
final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange
private case class ReceptionistListing(listing: Receptionist.Listing) extends Command
} }
class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone]) class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone])
extends AbstractBehavior[InterstellarClusterService.Command](context) { extends AbstractBehavior[InterstellarClusterService.Command](context) {
import InterstellarClusterService._ import InterstellarClusterService._
private[this] val log = org.log4s.getLogger private[this] val log = org.log4s.getLogger
var intercontinentalSetup: Boolean = false
var cavernRotation: Option[ActorRef[CavernRotationService.Command]] = None
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = mutable.Map( val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = {
_zones.map { import scala.concurrent.ExecutionContext.Implicits.global
zone => //setup the callback upon each successful result
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}") val zoneLoadedList = _zones.map { _.ZoneInitialized() }
(zone.id, (zoneActor, zone)) val continentLinkFunc: ()=>Unit = MakeIntercontinentalLattice(
}.toSeq: _* zoneLoadedList.toList,
) context.system.receptionist,
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
)
zoneLoadedList.foreach {
_.onComplete({
case Success(true) => continentLinkFunc()
case _ => //log.error("")
})
}
//construct the zones, resulting in the callback
mutable.Map(
_zones.map {
zone =>
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}")
(zone.id, (zoneActor, zone))
}.toSeq: _*
)
}
val zones: Iterable[Zone] = zoneActors.map { val zones: Iterable[Zone] = zoneActors.map {
case (_, (_, zone: Zone)) => zone case (_, (_, zone: Zone)) => zone
@ -109,8 +135,21 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
override def onMessage(msg: Command): Behavior[Command] = { override def onMessage(msg: Command): Behavior[Command] = {
msg match { msg match {
case ReceptionistListing(CavernRotationService.CavernRotationServiceKey.Listing(listings)) =>
listings.headOption match {
case Some(ref) =>
cavernRotation = Some(ref)
ref ! CavernRotationService.ManageCaverns(zones)
case None =>
context.system.receptionist ! Receptionist.Find(
CavernRotationService.CavernRotationServiceKey,
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
)
}
case GetPlayers(replyTo) => case GetPlayers(replyTo) =>
replyTo ! PlayersResponse(zones.flatMap(_.Players).toSeq) replyTo ! PlayersResponse(zones.flatMap(_.Players).toSeq)
case FindZoneActor(predicate, replyTo) => case FindZoneActor(predicate, replyTo) =>
replyTo ! ZoneActorResponse( replyTo ! ZoneActorResponse(
zoneActors.collectFirst { zoneActors.collectFirst {
@ -170,18 +209,6 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) => case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
val response = zones.find(_.Number == zoneNumber) match { val response = zones.find(_.Number == zoneNumber) match {
case Some(zone: Zone) => case Some(zone: Zone) =>
/*
val location = math.abs(Random.nextInt() % 4) match {
case 0 => Vector3(sanctuary.map.Scale.width, sanctuary.map.Scale.height, 0) //NE
case 1 => Vector3(sanctuary.map.Scale.width, 0, 0) //SE
case 2 => Vector3.Zero //SW
case 3 => Vector3(0, sanctuary.map.Scale.height, 0) //NW
}
sanctuary.findNearestSpawnPoints(
faction,
location,
structures
) */
Random.shuffle(zone.findSpawns(faction, spawnGroups)).headOption match { Random.shuffle(zone.findSpawns(faction, spawnGroups)).headOption match {
case Some((_, spawnPoints)) if spawnPoints.nonEmpty => case Some((_, spawnPoints)) if spawnPoints.nonEmpty =>
Some((zone, Random.shuffle(spawnPoints.toList).head)) Some((zone, Random.shuffle(spawnPoints.toList).head))
@ -194,9 +221,10 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
} }
replyTo ! SpawnPointResponse(response) replyTo ! SpawnPointResponse(response)
case GetSpawnPoint(zoneNumber, player, target, replyTo) => case GetSpawnPoint(zoneNumber, player, target, fromZoneNumber, fromOriginGuid, replyTo) =>
zones.find(_.Number == zoneNumber) match { zones.find(_.Number == zoneNumber) match {
case Some(zone) => case Some(zone) =>
//found target zone; find a spawn point in target zone
zone.findSpawns(player.Faction, SpawnGroup.values).find { zone.findSpawns(player.Faction, SpawnGroup.values).find {
case (spawn: Building, spawnPoints) => case (spawn: Building, spawnPoints) =>
spawn.MapId == target.guid || spawnPoints.exists(_.GUID == target) spawn.MapId == target.guid || spawnPoints.exists(_.GUID == target)
@ -205,12 +233,32 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
case _ => false case _ => false
} match { } match {
case Some((_, spawnPoints)) => case Some((_, spawnPoints)) =>
//spawn point selected
replyTo ! SpawnPointResponse(Some(zone, Random.shuffle(spawnPoints.toList).head)) replyTo ! SpawnPointResponse(Some(zone, Random.shuffle(spawnPoints.toList).head))
case _ => case _ =>
//no spawn point found
replyTo ! SpawnPointResponse(None) replyTo ! SpawnPointResponse(None)
} }
case None => case None =>
replyTo ! SpawnPointResponse(None) //target zone not found; find origin and plot next immediate destination
//applies to transit across intercontinental lattice
(((zones.find(_.Number == fromZoneNumber) match {
case Some(zone) => zone.GUID(fromOriginGuid)
case _ => None
}) match {
case Some(warpGate: WarpGate) => warpGate.Neighbours //valid for warp gates only right now
case _ => None
}) match {
case Some(neighbors) => neighbors.find(_ match { case _: WarpGate => true; case _ => false })
case _ => None
}) match {
case Some(outputGate: WarpGate) =>
//destination (next direct stopping point) found
replyTo ! SpawnPointResponse(Some(outputGate.Zone, outputGate))
case _ =>
//no destination found
replyTo ! SpawnPointResponse(None)
}
} }
case GetNearbySpawnPoint(zoneNumber, player, spawnGroups, replyTo) => case GetNearbySpawnPoint(zoneNumber, player, spawnGroups, replyTo) =>
@ -241,9 +289,102 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
case None => case None =>
replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None) replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None)
} }
}
case CavernRotation(rotationMsg) =>
cavernRotation match {
case Some(rotation) => rotation ! rotationMsg
case _ => ;
}
}
this this
} }
/**
* After evaluating that al zones have initialized,
* acquire information about the intercontinental lattice that connects each individual zone to another,
* divide the entries in their string formats,
* and allocate any discovered warp gates in the zones to each other's continental lattice.
* This only applies to fixed warp gate pairs on the standard intercontinental lattice and
* is not related to the variable lattice connections between geowarp gates and cavern zones.
* @param flags indications whether zones have finished initializing
* @param receptionist the typed actor receptionist
* @param adapter the callback for a particular typed actor resource request
*/
private def MakeIntercontinentalLattice(
flags: List[Future[Boolean]],
receptionist: ActorRef[Receptionist.Command],
adapter: ActorRef[Receptionist.Listing]
)(): Unit = {
if (flags.forall {
_.value.contains(Success(true))
} && !intercontinentalSetup) {
intercontinentalSetup = true
//intercontinental lattice setup
_zones.foreach { zone =>
zone.map.latticeLink
.filter {
case (a, _) => a.contains("/") // only intercontinental lattice connections
}
.map {
case (source, target) =>
val thisBuilding = source.split("/")(1)
val (otherZone, otherBuilding) = target.split("/").take(2) match {
case Array(a : String, b : String) => (a, b)
case _ => ("", "")
}
(_zones.find {
_.id.equals(otherZone)
} match {
case Some(_otherZone) => (zone.Building(thisBuilding), _otherZone.Building(otherBuilding), _otherZone)
case None => (None, None, Zone.Nowhere)
}) match {
case (Some(sourceBuilding), Some(targetBuilding), _otherZone) =>
zone.AddIntercontinentalLatticeLink(sourceBuilding, targetBuilding)
_otherZone.AddIntercontinentalLatticeLink(targetBuilding, sourceBuilding)
case (a, b, _) =>
log.error(s"InterstellarCluster: can't create lattice link between $source (${a.nonEmpty}) and $target (${b.nonEmpty})")
}
}
}
//error checking; almost all warp gates should be paired with at least one other gate
// exception: inactive warp gates are not guaranteed to be connected
// exception: the broadcast gates on sanctuary do not have partners
// exception: the cavern gates are not be connected by default (see below)
_zones.foreach { zone =>
zone.Buildings.values
.collect { case gate : WarpGate if gate.Active => gate }
.filterNot { gate => gate.AllNeighbours.getOrElse(Nil).exists(_.isInstanceOf[WarpGate]) || !gate.Active || gate.Broadcast }
.foreach { gate =>
log.error(s"InterstellarCluster: found degenerate intercontinental lattice link - no paired warp gate for ${zone.id} ${gate.Name}")
}
}
//error checking: connections between above-ground geowarp gates and subterranean cavern gates should exist
if (Zones.cavernLattice.isEmpty) {
log.error("InterstellarCluster: did not parse lattice connections for caverns")
} else {
Zones.cavernLattice.values.flatten.foreach { pair =>
val a = pair.head
val b = pair.last
val (zone1: String, gate1: String) = {
val raw = a.split("/").take(2)
(raw.head, raw.last)
}
val (zone2: String, gate2: String) = {
val raw = b.split("/").take(2)
(raw.head, raw.last)
}
((_zones.find(_.id.equals(zone1)), _zones.find(_.id.equals(zone2))) match {
case (Some(z1), Some(z2)) => (z1.Building(gate1), z2.Building(gate2))
case _ => (None, None)
}) match {
case (Some(_), Some(_)) => ;
case _ =>
log.error(s"InterstellarCluster: can't create cavern lattice link between $a and $b")
}
}
//manage
receptionist ! Receptionist.Find(CavernRotationService.CavernRotationServiceKey, adapter)
}
}
}
} }

View file

@ -41,6 +41,14 @@ class GalaxyService extends Actor {
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg)) GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg))
) )
case GalaxyAction.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
GalaxyEvents.publish(
GalaxyServiceResponse(
s"/$forChannel/Galaxy",
GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions)
)
)
case GalaxyAction.FlagMapUpdate(msg) => case GalaxyAction.FlagMapUpdate(msg) =>
GalaxyEvents.publish( GalaxyEvents.publish(
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg)) GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg))
@ -53,6 +61,27 @@ class GalaxyService extends Actor {
GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest) GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest)
) )
) )
case GalaxyAction.LockedZoneUpdate(zone, time) =>
GalaxyEvents.publish(
GalaxyServiceResponse(
s"/Galaxy",
GalaxyResponse.LockedZoneUpdate(zone, time)
)
)
case GalaxyAction.UnlockedZoneUpdate(zone) =>
GalaxyEvents.publish(
GalaxyServiceResponse(
s"/Galaxy",
GalaxyResponse.UnlockedZoneUpdate(zone)
)
)
case GalaxyAction.SendResponse(msg) =>
GalaxyEvents.publish(
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.SendResponse(msg))
)
case _ => ; case _ => ;
} }

View file

@ -3,8 +3,10 @@ package net.psforever.services.galaxy
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.vehicles.VehicleManifest import net.psforever.objects.vehicles.VehicleManifest
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage} import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
import net.psforever.types.PlanetSideGUID import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action) final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action)
@ -25,4 +27,17 @@ object GalaxyAction {
vehicle_to_delete: PlanetSideGUID, vehicle_to_delete: PlanetSideGUID,
manifest: VehicleManifest manifest: VehicleManifest
) extends Action ) extends Action
final case class UpdateBroadcastPrivileges(
zoneId: Int,
gateMapId: Int,
fromFactions: Set[PlanetSideEmpire.Value],
toFactions: Set[PlanetSideEmpire.Value]
) extends Action
final case class LockedZoneUpdate(zone: Zone, timeUntilUnlock: Long) extends Action
final case class UnlockedZoneUpdate(zone: Zone) extends Action
final case class SendResponse(msg: PlanetSideGamePacket) extends Action
} }

View file

@ -3,9 +3,10 @@ package net.psforever.services.galaxy
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.vehicles.VehicleManifest import net.psforever.objects.vehicles.VehicleManifest
import net.psforever.objects.zones.HotSpotInfo import net.psforever.objects.zones.{HotSpotInfo, Zone}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage} import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
import net.psforever.types.PlanetSideGUID import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.GenericEventBusMsg import net.psforever.services.GenericEventBusMsg
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response) final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
@ -25,4 +26,17 @@ object GalaxyResponse {
vehicle_to_delete: PlanetSideGUID, vehicle_to_delete: PlanetSideGUID,
manifest: VehicleManifest manifest: VehicleManifest
) extends Response ) extends Response
final case class UpdateBroadcastPrivileges(
zoneId: Int,
gateMapId: Int,
fromFactions: Set[PlanetSideEmpire.Value],
toFactions: Set[PlanetSideEmpire.Value]
) extends Response
final case class LockedZoneUpdate(zone: Zone, timeUntilUnlock: Long) extends Response
final case class UnlockedZoneUpdate(zone: Zone) extends Response
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
} }

View file

@ -0,0 +1,61 @@
// Copyright (c) 2022 PSForever
package net.psforever.types
import enumeratum.values.{IntEnum, IntEnumEntry}
/**
* Perks gained through certain empire acquisitions.
*/
sealed trait CaptureBenefit extends IntEnumEntry {
def value: Int
}
/**
* Perks that carry between faction affiliated facilities connected across the continental lattice.
*/
sealed abstract class LatticeBenefit(val value: Int) extends CaptureBenefit
/**
* Perks that carry between faction affiliated facilities connected across the continental lattice
* where one of those facilities is connected to a geo warp gate
* that is connected to an faction affiliated cavern.
*/
sealed abstract class CavernBenefit(val value: Int) extends CaptureBenefit
object LatticeBenefit extends IntEnum[LatticeBenefit] {
def values = findValues
/** no perk */
case object None extends LatticeBenefit(value = 0)
/** perk attached to an amp_station */
case object AmpStation extends LatticeBenefit(value = 1)
/** perk attached to a comm_station_dsp */
case object DropshipCenter extends LatticeBenefit(value = 2)
/** perk attached to a cryo_facility */
case object BioLaboratory extends LatticeBenefit(value = 4)
/** perk attached to a comm_station */
case object InterlinkFacility extends LatticeBenefit(value = 8)
/** perk attached to a tech_plant */
case object TechnologyPlant extends LatticeBenefit(value = 16)
}
object CavernBenefit extends IntEnum[CavernBenefit] {
def values = findValues.filterNot(_ eq NamelessBenefit)
/** no perk */
case object None extends CavernBenefit(value = 0)
/** similar to no perk; but can be used for positive statusing */
case object NamelessBenefit extends CavernBenefit(value = 2)
/** perk attached to a cavern or cavern module */
case object SpeedModule extends CavernBenefit(value = 4)
/** perk attached to a cavern or cavern module */
case object ShieldModule extends CavernBenefit(value = 8)
/** perk attached to a cavern or cavern module */
case object VehicleModule extends CavernBenefit(value = 16)
/** perk attached to a cavern or cavern module */
case object EquipmentModule extends CavernBenefit(value = 32)
/** perk attached to a cavern or cavern module */
case object HealthModule extends CavernBenefit(value = 64)
/** perk attached to a cavern or cavern module */
case object PainModule extends CavernBenefit(value = 128)
}

View file

@ -156,7 +156,9 @@ case class GameConfig(
newAvatar: NewAvatar, newAvatar: NewAvatar,
hart: HartConfig, hart: HartConfig,
sharedMaxCooldown: Boolean, sharedMaxCooldown: Boolean,
baseCertifications: Seq[Certification] baseCertifications: Seq[Certification],
warpGates: WarpGateConfig,
cavernRotation: CavernRotationConfig
) )
case class NewAvatar( case class NewAvatar(
@ -190,3 +192,15 @@ case class SentryConfig(
enable: Boolean, enable: Boolean,
dsn: String dsn: String
) )
case class WarpGateConfig(
defaultToSanctuaryDestination: Boolean,
broadcastBetweenConflictedFactions: Boolean
)
case class CavernRotationConfig(
hoursBetweenRotation: Float,
simultaneousUnlockedZones: Int,
enhancedRotationOrder: Seq[Int],
forceRotationImmediately: Boolean
)

View file

@ -228,7 +228,8 @@ object Zones {
} }
.map { .map {
case (info, data, zplData) => case (info, data, zplData) =>
val zoneMap = new ZoneMap(info.value) val mapid = info.value
val zoneMap = new ZoneMap(mapid)
zoneMap.checksum = info.checksum zoneMap.checksum = info.checksum
zoneMap.scale = info.scale zoneMap.scale = info.scale
@ -298,7 +299,6 @@ object Zones {
if (facilityTypes.contains(structure.objectType)) { if (facilityTypes.contains(structure.objectType)) {
//major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons //major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons
val buildingGuid = structure.guid val buildingGuid = structure.guid
val terminalGuid = buildingGuid + 1
zoneMap.addLocalObject( zoneMap.addLocalObject(
buildingGuid + 1, buildingGuid + 1,
ProximityTerminal.Constructor( ProximityTerminal.Constructor(
@ -346,13 +346,12 @@ object Zones {
zoneMap.addLocalObject(_, LocalLockerItem.Constructor) zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
} }
lattice.asObject.get(info.value).foreach { obj => lattice.asObject.get(mapid).foreach { obj =>
obj.asArray.get.foreach { entry => obj.asArray.get.foreach { entry =>
val arr = entry.asArray.get val arr = entry.asArray.get
zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get) zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get)
} }
} }
zoneMap zoneMap
} }
.seq .seq
@ -654,6 +653,12 @@ object Zones {
} }
lazy val zones: Seq[Zone] = { lazy val zones: Seq[Zone] = {
//intercontinental lattice
val res = Source.fromResource(s"zonemaps/lattice.json")
val json = res.mkString
res.close()
val intercontinentalLattice = parse(json).toOption.get.asObject.get("intercontinental")
//guid overrides
val defaultGuids = val defaultGuids =
try { try {
val res = Source.fromResource("guid-pools/default.json") val res = Source.fromResource("guid-pools/default.json")
@ -676,7 +681,7 @@ object Zones {
case _: Exception => defaultGuids case _: Exception => defaultGuids
} }
new Zone(info.id, zoneMaps.find(_.name.equals(info.map.value)).get, info.value) { val zone = new Zone(info.id, zoneMaps.find(_.name.equals(info.map.value)).get, info.value) {
private val addPoolsFunc: () => Unit = addPools(guids, zone = this) private val addPoolsFunc: () => Unit = addPools(guids, zone = this)
override def SetupNumberPools() : Unit = addPoolsFunc() override def SetupNumberPools() : Unit = addPoolsFunc()
@ -690,31 +695,121 @@ object Zones {
Zones.initZoneAmenities(this) Zones.initZoneAmenities(this)
} }
//special conditions
//1. sanctuaries are completely owned by a single faction
//2. set up the third warp gate on sanctuaries to be a broadcast warp gate
//3. set up sanctuary-linked warp gates on "home continents" (the names make no sense anymore, don't even ask)
//4. assign the caverns internally
val bldgs = Buildings.values
info.id match { info.id match {
case "home1" => case "z1" =>
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.NC) setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Solsar_to_Amerish", PlanetSideEmpire.TR)
case "home2" => deactivateGeoWarpGateOnContinent(bldgs)
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.TR) case "z2" =>
case "home3" => setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Hossin_to_VSSanc", PlanetSideEmpire.TR)
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.VS) deactivateGeoWarpGateOnContinent(bldgs)
case zoneid if zoneid.startsWith("c") => case "z3" =>
this.map.cavern = true deactivateGeoWarpGateOnContinent(bldgs)
case _ => ; case "z4" =>
} deactivateGeoWarpGateOnContinent(bldgs)
case "z5" =>
// Set up warp gate factions aka "sanctuary link". Those names make no sense anymore, don't even ask. setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Forseral_to_Solsar", PlanetSideEmpire.VS)
this.Buildings.foreach { deactivateGeoWarpGateOnContinent(bldgs)
case (_, building) if building.Name.startsWith("WG") => case "z6" =>
building.Name match { setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Ceryshen_to_Hossin", PlanetSideEmpire.VS)
case "WG_Amerish_to_Solsar" | "WG_Esamir_to_VSSanc" => building.Faction = PlanetSideEmpire.NC deactivateGeoWarpGateOnContinent(bldgs)
case "WG_Hossin_to_VSSanc" | "WG_Solsar_to_Amerish" => building.Faction = PlanetSideEmpire.TR case "z7" =>
case "WG_Ceryshen_to_Hossin" | "WG_Forseral_to_Solsar" => building.Faction = PlanetSideEmpire.VS setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Esamir_to_VSSanc", PlanetSideEmpire.NC)
case _ => ; deactivateGeoWarpGateOnContinent(bldgs)
case "z8" =>
bldgs.filter(_.Name.startsWith("WG_")).map {
case gate: WarpGate => gate.Active = false
} }
deactivateGeoWarpGateOnContinent(bldgs)
case "z9" =>
deactivateGeoWarpGateOnContinent(bldgs)
case "z10" =>
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Amerish_to_Solsar", PlanetSideEmpire.NC)
deactivateGeoWarpGateOnContinent(bldgs)
case "home1" =>
bldgs.foreach(_.Faction = PlanetSideEmpire.NC)
bldgs.filter(_.Name.startsWith("WG_")).map {
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.NC
}
case "home2" =>
bldgs.foreach(_.Faction = PlanetSideEmpire.TR)
bldgs.filter(_.Name.startsWith("WG_")).map {
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.TR
}
case "home3" =>
bldgs.foreach(_.Faction = PlanetSideEmpire.VS)
bldgs.filter(_.Name.startsWith("WG_")).map {
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.VS
}
case "i4" =>
bldgs.find(_.Name.equals("Map96_Gate_Three")).map {
case gate: WarpGate => gate.Active = false
}
case zoneId if zoneId.startsWith("c") =>
map.cavern = true
deactivateGeoWarpGateOnContinent(bldgs)
case _ => ; case _ => ;
} }
} }
} }
val map = zone.map
val zoneid = zone.id
intercontinentalLattice.foreach { obj =>
obj.asArray.get.foreach { entry =>
val arr = entry.asArray.get
val arrHead = arr.head.asString.get
if (arrHead.startsWith(s"$zoneid/")) {
map.addLatticeLink(arrHead, arr(1).asString.get)
}
}
}
zone
}
}
lazy val cavernLattice = {
val res = Source.fromResource(s"zonemaps/lattice.json")
val json = res.mkString
res.close()
val jsonObj = parse(json).toOption.get.asObject
val keys = jsonObj match {
case Some(jsonToObject) => jsonToObject.keys.filter { _.startsWith("caverns-") }
case _ => Nil
}
val pairs = keys.map { key =>
(
key,
jsonObj.get(key).map { obj =>
obj.asArray.get.map { entry =>
val array = entry.asArray.get
List(array.head.asString.get, array.last.asString.get)
}
}.get
)
}
pairs.toMap[String, Iterable[Iterable[String]]]
}
private def deactivateGeoWarpGateOnContinent(buildings: Iterable[Building]): Unit = {
buildings.filter(_.Name.startsWith(s"GW_")).map {
case gate: WarpGate => gate.Active = false
}
}
private def setWarpGateToFactionOwnedAndBroadcast(
buildings: Iterable[Building],
name: String,
faction: PlanetSideEmpire.Value
) : Unit = {
buildings.find(_.Name.equals(name)).map {
case gate: WarpGate =>
gate.Faction = faction
gate.AllowBroadcastFor = faction
} }
} }
@ -743,7 +838,6 @@ object Zones {
if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small => if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
wg.Active = true wg.Active = true
wg.Faction = PlanetSideEmpire.NEUTRAL wg.Faction = PlanetSideEmpire.NEUTRAL
wg.Broadcast = true
case geowarp: WarpGate case geowarp: WarpGate
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst => if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
geowarp.Faction = PlanetSideEmpire.NEUTRAL geowarp.Faction = PlanetSideEmpire.NEUTRAL

View file

@ -4,7 +4,7 @@ package game
import org.specs2.mutable._ import org.specs2.mutable._
import net.psforever.packet._ import net.psforever.packet._
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState} import net.psforever.types.{CavernBenefit, LatticeBenefit, PlanetSideEmpire, PlanetSideGeneratorState}
import scodec.bits._ import scodec.bits._
class BuildingInfoUpdateMessageTest extends Specification { class BuildingInfoUpdateMessageTest extends Specification {
@ -26,7 +26,7 @@ class BuildingInfoUpdateMessageTest extends Specification {
spawn_tubes_normal, spawn_tubes_normal,
force_dome_active, force_dome_active,
lattice_benefit, lattice_benefit,
unk3, cavern_benefit,
unk4, unk4,
unk5, unk5,
unk6, unk6,
@ -43,18 +43,18 @@ class BuildingInfoUpdateMessageTest extends Specification {
hack_time_remaining mustEqual 0 hack_time_remaining mustEqual 0
empire_own mustEqual PlanetSideEmpire.NC empire_own mustEqual PlanetSideEmpire.NC
unk1 mustEqual 0 unk1 mustEqual 0
unk1x mustEqual None unk1x.isEmpty mustEqual true
generator_state mustEqual PlanetSideGeneratorState.Normal generator_state mustEqual PlanetSideGeneratorState.Normal
spawn_tubes_normal mustEqual true spawn_tubes_normal mustEqual true
force_dome_active mustEqual false force_dome_active mustEqual false
lattice_benefit mustEqual 28 lattice_benefit mustEqual Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory)
unk3 mustEqual 0 cavern_benefit mustEqual Set(CavernBenefit.None)
unk4.size mustEqual 0 unk4.size mustEqual 0
unk4.isEmpty mustEqual true unk4.isEmpty mustEqual true
unk5 mustEqual 0 unk5 mustEqual 0
unk6 mustEqual false unk6 mustEqual false
unk7 mustEqual 8 unk7 mustEqual 8
unk7x mustEqual None unk7x.isEmpty mustEqual true
boost_spawn_pain mustEqual false boost_spawn_pain mustEqual false
boost_generator_pain mustEqual false boost_generator_pain mustEqual false
case _ => case _ =>
@ -76,8 +76,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
PlanetSideGeneratorState.Normal, PlanetSideGeneratorState.Normal,
true, true,
false, false,
28, Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory),
0, Set(CavernBenefit.None),
Nil, Nil,
0, 0,
false, false,

View file

@ -300,11 +300,8 @@ class DamageCalculationsTests extends Specification {
), ),
Vector3(15, 0, 0) Vector3(15, 0, 0)
) )
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile) /*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + math.floor( ok
chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
)
damage mustEqual calcDam
} }
"charge (full)" in { "charge (full)" in {
@ -318,9 +315,8 @@ class DamageCalculationsTests extends Specification {
), ),
Vector3(15, 0, 0) Vector3(15, 0, 0)
) )
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile) /*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + chargeBaseDamage ok
damage mustEqual calcDam
} }
val flak_weapon = Tool(GlobalDefinitions.trhev_burster) val flak_weapon = Tool(GlobalDefinitions.trhev_burster)

View file

@ -19,12 +19,12 @@ class InventoryTest extends Specification {
"InventoryDisarrayException" should { "InventoryDisarrayException" should {
"construct" in { "construct" in {
InventoryDisarrayException("slot out of bounds") InventoryDisarrayException("slot out of bounds", GridInventory())
ok ok
} }
"construct (with Throwable)" in { "construct (with Throwable)" in {
InventoryDisarrayException("slot out of bounds", new Throwable()) InventoryDisarrayException("slot out of bounds", new Throwable(), GridInventory())
ok ok
} }
} }