mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
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:
parent
546a4e4f0d
commit
ced228509c
|
|
@ -25,7 +25,7 @@ import net.psforever.services.chat.ChatService
|
|||
import net.psforever.services.galaxy.GalaxyService
|
||||
import net.psforever.services.properties.PropertyOverrideManager
|
||||
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.zones.Zones
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
|
@ -120,10 +120,6 @@ object Server {
|
|||
}
|
||||
|
||||
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
||||
|
||||
system.spawn(ChatService(), ChatService.ChatServiceKey.id)
|
||||
system.spawn(InterstellarClusterService(zones), InterstellarClusterService.InterstellarClusterServiceKey.id)
|
||||
|
||||
val serviceManager = ServiceManager.boot
|
||||
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
|
||||
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[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.world.port), session), "world-socket")
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,43 @@ game {
|
|||
standard_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 {
|
||||
|
|
|
|||
|
|
@ -1027,7 +1027,7 @@
|
|||
"Map99_Gate_Three"
|
||||
]
|
||||
],
|
||||
"udg01": [
|
||||
"ugd01": [
|
||||
[
|
||||
"N_Redoubt",
|
||||
"N_ATPlant"
|
||||
|
|
@ -1077,7 +1077,7 @@
|
|||
"S_ATPlant"
|
||||
]
|
||||
],
|
||||
"udg02": [
|
||||
"ugd02": [
|
||||
[
|
||||
"N_Redoubt",
|
||||
"N_ATPlant"
|
||||
|
|
@ -1127,7 +1127,7 @@
|
|||
"S_ATPlant"
|
||||
]
|
||||
],
|
||||
"udg03": [
|
||||
"ugd03": [
|
||||
[
|
||||
"NW_Redoubt",
|
||||
"NW_ATPlant"
|
||||
|
|
@ -1177,7 +1177,7 @@
|
|||
"SE_ATPlant"
|
||||
]
|
||||
],
|
||||
"udg04": [
|
||||
"ugd04": [
|
||||
[
|
||||
"N_Redoubt",
|
||||
"N_ATPlant"
|
||||
|
|
@ -1227,7 +1227,7 @@
|
|||
"S_ATPlant"
|
||||
]
|
||||
],
|
||||
"udg05": [
|
||||
"ugd05": [
|
||||
[
|
||||
"NW_Redoubt",
|
||||
"NW_ATPlant"
|
||||
|
|
@ -1281,7 +1281,7 @@
|
|||
"SE_ATPlant"
|
||||
]
|
||||
],
|
||||
"udg06": [
|
||||
"ugd06": [
|
||||
[
|
||||
"N_Redoubt",
|
||||
"N_ATPlant"
|
||||
|
|
@ -1330,5 +1330,423 @@
|
|||
"GW_Cavern6_S",
|
||||
"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"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.{Default, Player, Session}
|
||||
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.zones.Zoning
|
||||
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.services.chat.ChatService
|
||||
import net.psforever.services.chat.ChatService.ChatChannel
|
||||
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.concurrent.duration._
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.services.{CavernRotationService, InterstellarClusterService}
|
||||
import net.psforever.types.ChatMessageType.UNK_229
|
||||
|
||||
object ChatActor {
|
||||
def apply(
|
||||
|
|
@ -44,6 +47,63 @@ object ChatActor {
|
|||
|
||||
private case class ListingResponse(listing: Receptionist.Listing) 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(
|
||||
|
|
@ -57,14 +117,15 @@ class ChatActor(
|
|||
|
||||
implicit val ec: ExecutionContextExecutor = context.executionContext
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
var channels: List[ChatChannel] = List()
|
||||
var session: Option[Session] = None
|
||||
var chatService: Option[ActorRef[ChatService.Command]] = None
|
||||
var silenceTimer: Cancellable = Default.Cancellable
|
||||
private[this] val log = org.log4s.getLogger
|
||||
var channels: List[ChatChannel] = List()
|
||||
var session: Option[Session] = None
|
||||
var chatService: Option[ActorRef[ChatService.Command]] = None
|
||||
var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
||||
var silenceTimer: Cancellable = Default.Cancellable
|
||||
|
||||
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(
|
||||
|
|
@ -72,43 +133,63 @@ class ChatActor(
|
|||
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||
)
|
||||
|
||||
context.system.receptionist ! Receptionist.Find(
|
||||
InterstellarClusterService.InterstellarClusterServiceKey,
|
||||
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||
)
|
||||
|
||||
def start(): Behavior[Command] = {
|
||||
Behaviors
|
||||
.receiveMessage[Command] {
|
||||
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
|
||||
chatService = Some(listings.head)
|
||||
channels ++= List(ChatChannel.Default())
|
||||
postStartBehaviour()
|
||||
case ListingResponse(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
|
||||
listings.headOption match {
|
||||
case Some(ref) =>
|
||||
cluster = Some(ref)
|
||||
postStartBehaviour()
|
||||
case None =>
|
||||
context.system.receptionist ! Receptionist.Find(
|
||||
InterstellarClusterService.InterstellarClusterServiceKey,
|
||||
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||
)
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
case SetSession(newSession) =>
|
||||
session = Some(newSession)
|
||||
postStartBehaviour()
|
||||
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
|
||||
chatService = Some(listings.head)
|
||||
channels ++= List(ChatChannel.Default())
|
||||
postStartBehaviour()
|
||||
|
||||
case other =>
|
||||
buffer.stash(other)
|
||||
Behaviors.same
|
||||
}
|
||||
case SetSession(newSession) =>
|
||||
session = Some(newSession)
|
||||
postStartBehaviour()
|
||||
|
||||
case other =>
|
||||
buffer.stash(other)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
def postStartBehaviour(): Behavior[Command] = {
|
||||
(session, chatService) match {
|
||||
case (Some(session), Some(chatService)) if session.player != null =>
|
||||
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, ChatChannel.Default())
|
||||
buffer.unstashAll(active(session, chatService))
|
||||
(session, chatService, cluster) match {
|
||||
case (Some(_session), Some(_chatService), Some(_cluster)) if _session.player != null =>
|
||||
_chatService ! ChatService.JoinChannel(chatServiceAdapter, _session, ChatChannel.Default())
|
||||
buffer.unstashAll(active(_session, _chatService, _cluster))
|
||||
case _ =>
|
||||
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._
|
||||
|
||||
Behaviors
|
||||
.receiveMessagePartial[Command] {
|
||||
case SetSession(newSession) =>
|
||||
active(newSession, chatService)
|
||||
active(newSession, chatService,cluster)
|
||||
|
||||
case JoinChannel(channel) =>
|
||||
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
|
||||
|
|
@ -275,10 +356,63 @@ class ChatActor(
|
|||
}
|
||||
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 */
|
||||
case (messageType, recipient, contents) if contents.startsWith("!") =>
|
||||
(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(
|
||||
session,
|
||||
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
||||
|
|
@ -293,8 +427,8 @@ class ChatActor(
|
|||
log.info(loc)
|
||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
|
||||
|
||||
case (_, _, contents) if contents.startsWith("!list") =>
|
||||
val zone = contents.split(" ").lift(1) match {
|
||||
case (_, _, content) if content.startsWith("!list") =>
|
||||
val zone = content.split(" ").lift(1) match {
|
||||
case None =>
|
||||
Some(session.zone)
|
||||
case Some(id) =>
|
||||
|
|
@ -302,7 +436,7 @@ class ChatActor(
|
|||
}
|
||||
|
||||
zone match {
|
||||
case Some(zone) =>
|
||||
case Some(inZone) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(
|
||||
CMT_GMOPEN,
|
||||
|
|
@ -313,7 +447,7 @@ class ChatActor(
|
|||
)
|
||||
)
|
||||
|
||||
(zone.LivePlayers ++ zone.Corpses)
|
||||
(inZone.LivePlayers ++ inZone.Corpses)
|
||||
.filter(_.CharId != session.player.CharId)
|
||||
.sortBy(p => (p.Name, !p.isAlive))
|
||||
.foreach(player => {
|
||||
|
|
@ -323,7 +457,7 @@ class ChatActor(
|
|||
CMT_GMOPEN,
|
||||
message.wideContents,
|
||||
"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
|
||||
)
|
||||
)
|
||||
|
|
@ -340,8 +474,8 @@ class ChatActor(
|
|||
)
|
||||
}
|
||||
|
||||
case (_, _, contents) if contents.startsWith("!ntu") && gmCommandAllowed =>
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
case (_, _, content) if content.startsWith("!ntu") && gmCommandAllowed =>
|
||||
val buffer = content.toLowerCase.split("\\s+")
|
||||
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), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
||||
|
|
@ -363,39 +497,16 @@ class ChatActor(
|
|||
session.zone.Buildings.values
|
||||
})
|
||||
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
|
||||
if (silos.isEmpty) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $facility", None)
|
||||
)
|
||||
}
|
||||
customNtuValue 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
|
||||
)
|
||||
}
|
||||
}
|
||||
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent=s"$facility")
|
||||
|
||||
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
|
||||
case Some("-list") | Some("-l") =>
|
||||
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
|
||||
case _ =>
|
||||
CavernRotationService.HurryNextRotation
|
||||
})
|
||||
|
||||
case _ =>
|
||||
// unknown ! commands are ignored
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package net.psforever.actors.session
|
|||
|
||||
import akka.actor.typed.receptionist.Receptionist
|
||||
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.util.Timeout
|
||||
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.equipment._
|
||||
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.serverobject.affinity.FactionAffinity
|
||||
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.objectcreate._
|
||||
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
|
||||
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
||||
import net.psforever.services.ServiceManager.{Lookup, LookupResult}
|
||||
import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
|
||||
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.hart.HartTimer
|
||||
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.util.{Config, DefinitionUtil}
|
||||
import net.psforever.zones.Zones
|
||||
|
|
@ -289,6 +291,118 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
var respawnTimer: 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): Unit = {
|
||||
|
|
@ -419,6 +533,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case ICS.InterstellarClusterServiceKey.Listing(listings) =>
|
||||
cluster = listings.head
|
||||
|
||||
case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
|
||||
listings.head ! SendCavernRotationUpdates(self)
|
||||
|
||||
// Avatar subscription update
|
||||
case avatar: Avatar =>
|
||||
/*
|
||||
|
|
@ -601,6 +718,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case GalaxyResponse.MapUpdate(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) =>
|
||||
sendResponse(msg)
|
||||
|
||||
|
|
@ -651,6 +778,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
//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) =>
|
||||
|
|
@ -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))
|
||||
case None =>
|
||||
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
|
||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||
if (Config.app.game.warpGates.defaultToSanctuaryDestination) {
|
||||
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()
|
||||
//VanuModuleUpdateMessage()
|
||||
//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(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
|
||||
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
|
||||
}
|
||||
ServiceManager.receptionist ! Receptionist.Find(
|
||||
CavernRotationService.CavernRotationServiceKey,
|
||||
context.self
|
||||
)
|
||||
LivePlayerList.Add(avatar.id, avatar)
|
||||
//PropertyOverrideMessage
|
||||
|
||||
|
|
@ -1133,7 +1281,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
|
||||
//the following subscriptions last until character switch/logout
|
||||
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.id}") //channel will be player.CharId (in order to work with packets)
|
||||
player.Zone match {
|
||||
|
|
@ -2085,15 +2233,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false))
|
||||
//cleanup
|
||||
(old_holsters ++ old_inventory).foreach {
|
||||
case (obj, guid) =>
|
||||
sendResponse(ObjectDeleteMessage(guid, 0))
|
||||
case (obj, objGuid) =>
|
||||
sendResponse(ObjectDeleteMessage(objGuid, 0))
|
||||
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||
}
|
||||
//redraw
|
||||
if (maxhand) {
|
||||
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
||||
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
||||
0
|
||||
slot = 0
|
||||
))
|
||||
}
|
||||
ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
|
||||
|
|
@ -2147,13 +2295,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||
case Some(carrier: Player) =>
|
||||
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.")
|
||||
}
|
||||
case _ =>
|
||||
log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid")
|
||||
}
|
||||
|
||||
case _ => ; // Nothing to drop, do nothing.
|
||||
}
|
||||
}
|
||||
|
|
@ -5653,7 +5800,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
) =>
|
||||
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
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(vehicle), _) =>
|
||||
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
|
||||
|
|
@ -5665,6 +5812,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
destinationZoneGuid.guid,
|
||||
player,
|
||||
destinationBuildingGuid,
|
||||
continent.Number,
|
||||
building_guid,
|
||||
context.self
|
||||
)
|
||||
log.info(s"${player.Name} wants to use a warp gate")
|
||||
|
|
@ -5949,7 +6098,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
summary,
|
||||
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")
|
||||
|
||||
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 = {
|
||||
building match {
|
||||
case wg: WarpGate =>
|
||||
sendResponse(
|
||||
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(building.infoUpdateMessage())
|
||||
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0)))
|
||||
//TODO one faction knows which gates are broadcast for another faction?
|
||||
sendResponse(
|
||||
BroadcastWarpgateUpdateMessage(
|
||||
continentNumber,
|
||||
buildingNumber,
|
||||
wg.Broadcast(PlanetSideEmpire.TR),
|
||||
wg.Broadcast(PlanetSideEmpire.NC),
|
||||
wg.Broadcast(PlanetSideEmpire.VS)
|
||||
if (wg.Broadcast(player.Faction)) {
|
||||
sendResponse(
|
||||
BroadcastWarpgateUpdateMessage(
|
||||
continentNumber,
|
||||
buildingNumber,
|
||||
player.Faction
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
|
@ -7572,6 +7696,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
|
||||
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
|
||||
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
|
||||
ImmediateDisconnect()
|
||||
} else {
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Vehicle) if !obj.Destroyed =>
|
||||
|
|
@ -8776,8 +8901,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
//for other zones ...
|
||||
//biolabs have/grant benefits
|
||||
val cryoBenefit: Float = toSpawnPoint.Owner match {
|
||||
case b: Building if b.hasLatticeBenefit(GlobalDefinitions.cryo_facility) => 0.5f
|
||||
case _ => 1f
|
||||
case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) => 0.5f
|
||||
case _ => 1f
|
||||
}
|
||||
//TODO cumulative death penalty
|
||||
toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds
|
||||
|
|
@ -9129,7 +9254,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
def KeepAlivePersistence(): Unit = {
|
||||
interimUngunnedVehicle = None
|
||||
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 {
|
||||
case mode: ChargeFireModeDefinition =>
|
||||
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 _ =>
|
||||
ProjectileQuality.Normal
|
||||
|
|
|
|||
|
|
@ -1,33 +1,44 @@
|
|||
package net.psforever.actors.zone
|
||||
|
||||
import akka.{actor => classic}
|
||||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
|
||||
import akka.{actor => classic}
|
||||
import net.psforever.actors.commands.NtuCommand
|
||||
import net.psforever.objects.NtuContainer
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
|
||||
import net.psforever.actors.zone.building._
|
||||
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.persistence
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.services.{InterstellarClusterService, Service, ServiceManager}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState}
|
||||
import net.psforever.util.Database._
|
||||
import net.psforever.services.{InterstellarClusterService, ServiceManager}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.util.Database.ctx
|
||||
import org.log4s.Logger
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
final case class BuildingControlDetails(
|
||||
galaxyService: classic.ActorRef = null,
|
||||
interstellarCluster: ActorRef[InterstellarClusterService.Command] = null
|
||||
)
|
||||
|
||||
object BuildingActor {
|
||||
def apply(zone: Zone, building: Building): Behavior[Command] =
|
||||
Behaviors
|
||||
.supervise[Command] {
|
||||
Behaviors.withStash(100) { buffer =>
|
||||
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building).start())
|
||||
Behaviors.withStash(capacity = 100) { buffer =>
|
||||
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)
|
||||
|
|
@ -40,13 +51,7 @@ object BuildingActor {
|
|||
|
||||
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command
|
||||
|
||||
final case class UpdateForceDome(state: Option[Boolean]) extends Command
|
||||
|
||||
object UpdateForceDome {
|
||||
def apply(): UpdateForceDome = UpdateForceDome(None)
|
||||
|
||||
def apply(state: Boolean): UpdateForceDome = UpdateForceDome(Some(state))
|
||||
}
|
||||
final case class AlertToFactionChange(building: Building) extends Command
|
||||
|
||||
// TODO remove
|
||||
// Changes to building objects should go through BuildingActor
|
||||
|
|
@ -70,55 +75,93 @@ object BuildingActor {
|
|||
final case class PowerOff() extends Command
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Set a facility affiliated to one faction to be affiliated to a different faction.
|
||||
* @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 invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
|
||||
building.Faction == PlanetSideEmpire.NEUTRAL ||
|
||||
building.NtuLevel == 0 ||
|
||||
(building.Generator match {
|
||||
case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed
|
||||
case _ => false
|
||||
})
|
||||
def setFactionTo(
|
||||
details: BuildingWrapper,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
log: BuildingWrapper => Logger
|
||||
): Unit = {
|
||||
setFactionInDatabase(details, faction, log)
|
||||
setFactionOnEntity(details, faction, log)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Set a facility affiliated to one faction to be affiliated to a different faction.
|
||||
* Handle the database entry updates to reflect the proper 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 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
|
||||
}
|
||||
def setFactionInDatabase(
|
||||
details: BuildingWrapper,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
log: BuildingWrapper => Logger
|
||||
): Unit = {
|
||||
val building = details.building
|
||||
val zone = building.Zone
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
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(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],
|
||||
buffer: StashBuffer[BuildingActor.Command],
|
||||
zone: Zone,
|
||||
building: Building
|
||||
building: Building,
|
||||
logic: BuildingLogic
|
||||
) {
|
||||
|
||||
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] = {
|
||||
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 {
|
||||
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
|
||||
interstellarCluster = listings.headOption
|
||||
postStartBehaviour()
|
||||
switchToBehavior(details.copy(interstellarCluster = listings.head))
|
||||
|
||||
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
|
||||
request match {
|
||||
case "galaxy" => galaxyService = Some(endpoint)
|
||||
}
|
||||
postStartBehaviour()
|
||||
switchToBehavior(request match {
|
||||
case "galaxy" => details.copy(galaxyService = endpoint)
|
||||
case _ => details
|
||||
})
|
||||
|
||||
case other =>
|
||||
buffer.stash(other)
|
||||
Behaviors.same
|
||||
setup(details)
|
||||
}
|
||||
}
|
||||
|
||||
def postStartBehaviour(): Behavior[Command] = {
|
||||
(galaxyService, interstellarCluster) match {
|
||||
case (Some(_galaxyService), Some(_interstellarCluster)) =>
|
||||
buffer.unstashAll(active(_galaxyService, _interstellarCluster))
|
||||
case _ =>
|
||||
Behaviors.same
|
||||
def switchToBehavior(details: BuildingControlDetails): Behavior[Command] = {
|
||||
if (details.galaxyService != null && details.interstellarCluster != null) {
|
||||
buffer.unstashAll(active(logic.wrapper(building, context, details)))
|
||||
} else {
|
||||
setup(details)
|
||||
}
|
||||
}
|
||||
|
||||
def active(
|
||||
galaxyService: classic.ActorRef,
|
||||
interstellarCluster: ActorRef[InterstellarClusterService.Command]
|
||||
): Behavior[Command] = {
|
||||
def active(details: BuildingWrapper): Behavior[Command] = {
|
||||
Behaviors.receiveMessagePartial {
|
||||
case SetFaction(faction) =>
|
||||
setFactionTo(faction, galaxyService)
|
||||
logic.setFactionTo(details, faction)
|
||||
|
||||
case AlertToFactionChange(neighbor) =>
|
||||
logic.alertToFactionChange(details, neighbor)
|
||||
Behaviors.same
|
||||
|
||||
case MapUpdate() =>
|
||||
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
|
||||
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
|
||||
Behaviors.same
|
||||
|
||||
case UpdateForceDome(stateOpt) =>
|
||||
stateOpt match {
|
||||
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 AmenityStateChange(amenity, data) =>
|
||||
logic.amenityStateChange(details, amenity, data)
|
||||
|
||||
case PowerOff() =>
|
||||
building.Generator match {
|
||||
case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted()
|
||||
case _ => powerLost()
|
||||
}
|
||||
Behaviors.same
|
||||
logic.powerOff(details)
|
||||
|
||||
case PowerOn() =>
|
||||
building.Generator match {
|
||||
case Some(gen) if building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu()
|
||||
case _ => powerRestored()
|
||||
}
|
||||
Behaviors.same
|
||||
logic.powerOn(details)
|
||||
|
||||
case msg @ NtuDepleted() =>
|
||||
// Someone let the base run out of nanites. No one gets anything.
|
||||
building.Amenities.foreach { amenity =>
|
||||
amenity.Actor ! msg
|
||||
}
|
||||
setFactionTo(PlanetSideEmpire.NEUTRAL, galaxyService)
|
||||
hasNtuSupply = false
|
||||
Behaviors.same
|
||||
case NtuDepleted() =>
|
||||
logic.ntuDepleted(details)
|
||||
|
||||
case msg @ SuppliedWithNtu() =>
|
||||
// Auto-repair restart, mainly. If the Generator works, power should be restored too.
|
||||
hasNtuSupply = true
|
||||
building.Amenities.foreach { amenity =>
|
||||
amenity.Actor ! msg
|
||||
}
|
||||
Behaviors.same
|
||||
case SuppliedWithNtu() =>
|
||||
logic.suppliedWithNtu(details)
|
||||
|
||||
case Ntu(msg) =>
|
||||
ntu(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
|
||||
logic.ntu(details, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
|||
import net.psforever.objects.ballistics.SourceEntry
|
||||
import net.psforever.objects.ce.Deployable
|
||||
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.blockmap.{BlockMapEntity, SectorGroup}
|
||||
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
|
||||
|
|
@ -13,6 +13,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
|||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.actors.zone.building.MajorFacilityLogic
|
||||
import net.psforever.util.Database._
|
||||
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 {
|
||||
case Success(buildings) =>
|
||||
var capitol: Option[Building] = None
|
||||
buildings.foreach { building =>
|
||||
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) =>
|
||||
b.Faction = PlanetSideEmpire(building.factionId)
|
||||
if(b.IsCapitol) {
|
||||
capitol = Some(b)
|
||||
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
|
||||
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
|
||||
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
|
||||
}
|
||||
}
|
||||
capitol match {
|
||||
case Some(b) => b.ForceDomeActive = BuildingActor.checkForceDomeStatus(b).getOrElse(false)
|
||||
case None => ;
|
||||
}
|
||||
case Failure(e) => log.error(e.getMessage)
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +119,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
case PickupItem(guid) =>
|
||||
zone.Ground ! Zone.Ground.PickupItem(guid)
|
||||
|
||||
case BuildDeployable(obj, tool) =>
|
||||
case BuildDeployable(obj, _) =>
|
||||
zone.Deployables ! Zone.Deployable.Build(obj)
|
||||
|
||||
case DismissDeployable(obj) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import net.psforever.objects.vital._
|
|||
import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3}
|
||||
import net.psforever.types._
|
||||
import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition}
|
||||
import net.psforever.objects.serverobject.terminals.tabs._
|
||||
import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier
|
||||
|
||||
import scala.annotation.switch
|
||||
|
|
@ -1261,11 +1262,33 @@ object GlobalDefinitions {
|
|||
/*
|
||||
Buildings
|
||||
*/
|
||||
val building = new BuildingDefinition(474) { Name = "building" } //borrows object id of entity mainbase1
|
||||
val amp_station = new BuildingDefinition(45) { Name = "amp_station"; SOIRadius = 300 }
|
||||
val comm_station = new BuildingDefinition(211) { Name = "comm_station"; SOIRadius = 300 }
|
||||
val comm_station_dsp = new BuildingDefinition(212) { Name = "comm_station_dsp"; SOIRadius = 300 }
|
||||
val cryo_facility = new BuildingDefinition(215) { Name = "cryo_facility"; SOIRadius = 300 }
|
||||
val amp_station = new BuildingDefinition(45) {
|
||||
Name = "amp_station"
|
||||
SOIRadius = 300
|
||||
LatticeLinkBenefit = LatticeBenefit.AmpStation
|
||||
}
|
||||
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" }
|
||||
|
||||
|
|
@ -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_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)
|
||||
hst.Name = "hst"
|
||||
hst.UseRadius = 20.4810f
|
||||
|
|
@ -1309,23 +1348,6 @@ object GlobalDefinitions {
|
|||
hst.NoWarp += peregrine_flight
|
||||
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)
|
||||
warpgate.Name = "warpgate"
|
||||
warpgate.UseRadius = 301.8713f
|
||||
|
|
@ -1338,7 +1360,7 @@ object GlobalDefinitions {
|
|||
warpgate_cavern.UseRadius = 51.0522f
|
||||
warpgate_cavern.SOIRadius = 52
|
||||
warpgate_cavern.VehicleAllowance = true
|
||||
warpgate_cavern.SpecificPointFunc = SpawnPoint.Gate
|
||||
warpgate_cavern.SpecificPointFunc = SpawnPoint.HalfHighGate
|
||||
|
||||
val warpgate_small = new WarpGateDefinition(995)
|
||||
warpgate_small.Name = "warpgate_small"
|
||||
|
|
@ -3466,7 +3488,7 @@ object GlobalDefinitions {
|
|||
|
||||
maelstrom_grenade_damager.Name = "maelstrom_grenade_damager"
|
||||
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.Damage0 = 32
|
||||
|
|
@ -3953,7 +3975,7 @@ object GlobalDefinitions {
|
|||
ProjectileDefinition.CalculateDerivedFields(quasar_projectile)
|
||||
|
||||
radiator_cloud.Name = "radiator_cloud"
|
||||
radiator_cloud.Damage0 = 2
|
||||
radiator_cloud.Damage0 = 1 //2
|
||||
radiator_cloud.DamageAtEdge = 1.0f
|
||||
radiator_cloud.DamageRadius = 5f
|
||||
radiator_cloud.DamageToHealthOnly = true
|
||||
|
|
@ -5106,7 +5128,6 @@ object GlobalDefinitions {
|
|||
spiker.FireModes.head.AmmoSlotIndex = 0
|
||||
spiker.FireModes.head.Magazine = 25
|
||||
spiker.Tile = InventoryTile.Tile33
|
||||
//TODO the spiker is weird
|
||||
|
||||
mini_chaingun.Name = "mini_chaingun"
|
||||
mini_chaingun.Size = EquipmentSize.Rifle
|
||||
|
|
@ -5182,7 +5203,6 @@ object GlobalDefinitions {
|
|||
maelstrom.FireModes(2).Magazine = 150
|
||||
maelstrom.FireModes(2).RoundsPerShot = 10
|
||||
maelstrom.Tile = InventoryTile.Tile93
|
||||
//TODO the maelstrom is weird
|
||||
|
||||
phoenix.Name = "phoenix"
|
||||
phoenix.Size = EquipmentSize.Rifle
|
||||
|
|
@ -8835,7 +8855,7 @@ object GlobalDefinitions {
|
|||
colossus_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
||||
colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f)
|
||||
colossus_flight.MaxCapacitor = 156
|
||||
colossus_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor
|
||||
colossus_flight.DefaultCapacitor = colossus_flight.MaxCapacitor
|
||||
colossus_flight.CapacitorDrain = 16
|
||||
colossus_flight.CapacitorDrainSpecial = 3
|
||||
colossus_flight.CapacitorRecharge = 42
|
||||
|
|
@ -8889,7 +8909,7 @@ object GlobalDefinitions {
|
|||
peregrine_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
||||
peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f)
|
||||
peregrine_flight.MaxCapacitor = 156
|
||||
peregrine_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor
|
||||
peregrine_flight.DefaultCapacitor = peregrine_flight.MaxCapacitor
|
||||
peregrine_flight.CapacitorDrain = 16
|
||||
peregrine_flight.CapacitorDrainSpecial = 3
|
||||
peregrine_flight.CapacitorRecharge = 42
|
||||
|
|
@ -9303,21 +9323,29 @@ object GlobalDefinitions {
|
|||
spawn_terminal.Name = "spawn_terminal"
|
||||
spawn_terminal.Damageable = 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.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
order_terminal.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
||||
order_terminal.Tab += 0 -> {
|
||||
val tab = EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
tab.Exclude = List(CavernEquipmentQuestion)
|
||||
tab
|
||||
}
|
||||
order_terminal.Tab += 1 -> ArmorWithAmmoPage(
|
||||
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
||||
EquipmentTerminalDefinition.maxAmmo
|
||||
)
|
||||
order_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
order_terminal.Tab += 2 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||
)
|
||||
order_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminal.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
||||
order_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminal.Tab += 4 -> {
|
||||
val tab = InfantryLoadoutPage()
|
||||
tab.Exclude = List(CavernEquipmentQuestion)
|
||||
tab
|
||||
}
|
||||
order_terminal.SellEquipmentByDefault = true
|
||||
order_terminal.MaxHealth = 500
|
||||
order_terminal.Damageable = true
|
||||
|
|
@ -9328,61 +9356,75 @@ object GlobalDefinitions {
|
|||
order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f)
|
||||
|
||||
order_terminala.Name = "order_terminala"
|
||||
order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
order_terminala.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
||||
order_terminala.Tab += 0 -> {
|
||||
val tab = EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
tab.Exclude = List(NoCavernEquipmentRule)
|
||||
tab
|
||||
}
|
||||
order_terminala.Tab += 1 -> ArmorWithAmmoPage(
|
||||
EquipmentTerminalDefinition.suits,
|
||||
EquipmentTerminalDefinition.maxAmmo
|
||||
)
|
||||
order_terminala.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
order_terminala.Tab += 2 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||
)
|
||||
order_terminala.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminala.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
||||
order_terminala.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX
|
||||
order_terminala.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminala.Tab += 4 -> {
|
||||
val tab = InfantryLoadoutPage()
|
||||
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
|
||||
tab
|
||||
}
|
||||
order_terminala.SellEquipmentByDefault = true
|
||||
order_terminala.Damageable = false
|
||||
order_terminala.Repairable = false
|
||||
|
||||
order_terminalb.Name = "order_terminalb"
|
||||
order_terminalb.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
order_terminalb.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
||||
order_terminalb.Tab += 0 -> {
|
||||
val tab = EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
tab.Exclude = List(NoCavernEquipmentRule)
|
||||
tab
|
||||
}
|
||||
order_terminalb.Tab += 1 -> ArmorWithAmmoPage(
|
||||
EquipmentTerminalDefinition.suits,
|
||||
EquipmentTerminalDefinition.maxAmmo
|
||||
)
|
||||
order_terminalb.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
order_terminalb.Tab += 2 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||
)
|
||||
order_terminalb.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminalb.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
||||
order_terminalb.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX
|
||||
order_terminalb.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
order_terminalb.Tab += 4 -> {
|
||||
val tab = InfantryLoadoutPage()
|
||||
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
|
||||
tab
|
||||
}
|
||||
order_terminalb.SellEquipmentByDefault = true
|
||||
order_terminalb.Damageable = false
|
||||
order_terminalb.Repairable = false
|
||||
|
||||
vanu_equipment_term.Name = "vanu_equipment_term"
|
||||
vanu_equipment_term.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
||||
vanu_equipment_term.Tab += 0 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||
)
|
||||
vanu_equipment_term.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
||||
vanu_equipment_term.Tab += 1 -> ArmorWithAmmoPage(
|
||||
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
||||
EquipmentTerminalDefinition.maxAmmo
|
||||
)
|
||||
vanu_equipment_term.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
vanu_equipment_term.Tab += 2 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||
)
|
||||
vanu_equipment_term.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
vanu_equipment_term.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
||||
vanu_equipment_term.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
vanu_equipment_term.Tab += 4 -> InfantryLoadoutPage()
|
||||
vanu_equipment_term.SellEquipmentByDefault = true
|
||||
vanu_equipment_term.Damageable = false
|
||||
vanu_equipment_term.Repairable = false
|
||||
|
||||
cert_terminal.Name = "cert_terminal"
|
||||
val certs = Certification.values.filter(_.cost != 0)
|
||||
val page = OrderTerminalDefinition.CertificationPage(certs)
|
||||
val page = CertificationPage(certs)
|
||||
cert_terminal.Tab += 0 -> page
|
||||
cert_terminal.MaxHealth = 500
|
||||
cert_terminal.Damageable = true
|
||||
|
|
@ -9402,20 +9444,28 @@ object GlobalDefinitions {
|
|||
implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f)
|
||||
|
||||
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.Damageable = false //TODO 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
|
||||
//TODO will need geometry when Damageable = true
|
||||
|
||||
ground_vehicle_terminal.Name = "ground_vehicle_terminal"
|
||||
ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
||||
VehicleTerminalDefinition.groundVehicles,
|
||||
VehicleTerminalDefinition.trunk
|
||||
)
|
||||
ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
||||
ground_vehicle_terminal.Tab += 46769 -> {
|
||||
val tab = VehiclePage(
|
||||
VehicleTerminalDefinition.groundVehicles,
|
||||
VehicleTerminalDefinition.trunk
|
||||
)
|
||||
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.Damageable = true
|
||||
ground_vehicle_terminal.Repairable = true
|
||||
|
|
@ -9425,11 +9475,15 @@ object GlobalDefinitions {
|
|||
ground_vehicle_terminal.Geometry = vterm
|
||||
|
||||
air_vehicle_terminal.Name = "air_vehicle_terminal"
|
||||
air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
||||
air_vehicle_terminal.Tab += 46769 -> VehiclePage(
|
||||
VehicleTerminalDefinition.flight1Vehicles,
|
||||
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.Damageable = true
|
||||
air_vehicle_terminal.Repairable = true
|
||||
|
|
@ -9439,11 +9493,15 @@ object GlobalDefinitions {
|
|||
air_vehicle_terminal.Geometry = vterm
|
||||
|
||||
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.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.Damageable = true
|
||||
dropship_vehicle_terminal.Repairable = true
|
||||
|
|
@ -9453,11 +9511,19 @@ object GlobalDefinitions {
|
|||
dropship_vehicle_terminal.Geometry = vterm
|
||||
|
||||
vehicle_terminal_combined.Name = "vehicle_terminal_combined"
|
||||
vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
||||
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles,
|
||||
VehicleTerminalDefinition.trunk
|
||||
)
|
||||
vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
||||
vehicle_terminal_combined.Tab += 46769 -> {
|
||||
val tab = VehiclePage(
|
||||
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles,
|
||||
VehicleTerminalDefinition.trunk
|
||||
)
|
||||
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.Damageable = true
|
||||
vehicle_terminal_combined.Repairable = true
|
||||
|
|
@ -9467,11 +9533,11 @@ object GlobalDefinitions {
|
|||
vehicle_terminal_combined.Geometry = vterm
|
||||
|
||||
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.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.Damageable = true
|
||||
vanu_air_vehicle_term.Repairable = true
|
||||
|
|
@ -9480,11 +9546,11 @@ object GlobalDefinitions {
|
|||
vanu_air_vehicle_term.Subtract.Damage1 = 8
|
||||
|
||||
vanu_vehicle_term.Name = "vanu_vehicle_term"
|
||||
vanu_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
||||
vanu_vehicle_term.Tab += 46769 -> VehiclePage(
|
||||
VehicleTerminalDefinition.groundVehicles,
|
||||
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.Damageable = true
|
||||
vanu_vehicle_term.Repairable = true
|
||||
|
|
@ -9493,19 +9559,21 @@ object GlobalDefinitions {
|
|||
vanu_vehicle_term.Subtract.Damage1 = 8
|
||||
|
||||
bfr_terminal.Name = "bfr_terminal"
|
||||
bfr_terminal.Tab += 0 -> OrderTerminalDefinition.VehiclePage(
|
||||
bfr_terminal.Tab += 0 -> VehiclePage(
|
||||
VehicleTerminalDefinition.bfrVehicles,
|
||||
VehicleTerminalDefinition.trunk
|
||||
)
|
||||
bfr_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage(
|
||||
bfr_terminal.Tab += 1 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
||||
) //inaccessible?
|
||||
bfr_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
bfr_terminal.Tab += 2 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
|
||||
) //inaccessible?
|
||||
bfr_terminal.Tab += 3 -> OrderTerminalDefinition.BattleframeSpawnLoadoutPage(
|
||||
VehicleTerminalDefinition.bfrVehicles
|
||||
)
|
||||
bfr_terminal.Tab += 3 -> {
|
||||
val tab = BattleframeSpawnLoadoutPage(VehicleTerminalDefinition.bfrVehicles)
|
||||
tab.Exclude = List(CavernEquipmentQuestion)
|
||||
tab
|
||||
}
|
||||
bfr_terminal.MaxHealth = 500
|
||||
bfr_terminal.Damageable = true
|
||||
bfr_terminal.Repairable = true
|
||||
|
|
@ -9521,7 +9589,7 @@ object GlobalDefinitions {
|
|||
respawn_tube.Damageable = true
|
||||
respawn_tube.DamageableByFriendlyFire = false
|
||||
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.Subtract.Damage1 = 8
|
||||
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.DamageableByFriendlyFire = false
|
||||
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
|
||||
|
||||
respawn_tube_tower.Name = "respawn_tube_tower"
|
||||
|
|
@ -9543,18 +9611,18 @@ object GlobalDefinitions {
|
|||
respawn_tube_tower.Damageable = true
|
||||
respawn_tube_tower.DamageableByFriendlyFire = false
|
||||
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.Subtract.Damage1 = 8
|
||||
respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
|
||||
|
||||
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.Repairable = false
|
||||
|
||||
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.Repairable = false
|
||||
|
||||
|
|
@ -9791,38 +9859,54 @@ object GlobalDefinitions {
|
|||
lodestar_repair_terminal.Repairable = false
|
||||
|
||||
multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal"
|
||||
multivehicle_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(
|
||||
multivehicle_rearm_terminal.Tab += 3 -> EquipmentPage(
|
||||
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.Damageable = false
|
||||
multivehicle_rearm_terminal.Repairable = false
|
||||
|
||||
bfr_rearm_terminal.Name = "bfr_rearm_terminal"
|
||||
bfr_rearm_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage(
|
||||
bfr_rearm_terminal.Tab += 1 -> EquipmentPage(
|
||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
||||
)
|
||||
bfr_rearm_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
||||
bfr_rearm_terminal.Tab += 2 -> EquipmentPage(
|
||||
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.Damageable = false
|
||||
bfr_rearm_terminal.Repairable = false
|
||||
|
||||
air_rearm_terminal.Name = "air_rearm_terminal"
|
||||
air_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
||||
air_rearm_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||
air_rearm_terminal.Tab += 4 -> {
|
||||
val tab = VehicleLoadoutPage(10)
|
||||
tab.Exclude = List(CavernEquipmentQuestion)
|
||||
tab
|
||||
}
|
||||
air_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
||||
air_rearm_terminal.Damageable = false
|
||||
air_rearm_terminal.Repairable = false
|
||||
|
||||
ground_rearm_terminal.Name = "ground_rearm_terminal"
|
||||
ground_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(
|
||||
ground_rearm_terminal.Tab += 3 -> EquipmentPage(
|
||||
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.Damageable = false
|
||||
ground_rearm_terminal.Repairable = false
|
||||
|
|
@ -9942,7 +10026,7 @@ object GlobalDefinitions {
|
|||
generator.Damageable = true
|
||||
generator.DamageableByFriendlyFire = false
|
||||
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.RepairIfDestroyed = true
|
||||
generator.Subtract.Damage1 = 9
|
||||
|
|
|
|||
|
|
@ -212,8 +212,12 @@ class Player(var avatar: Avatar)
|
|||
def HolsterItems(): List[InventoryItem] = holsters
|
||||
.zipWithIndex
|
||||
.collect {
|
||||
case (slot: EquipmentSlot, index: Int) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index)
|
||||
}.toList
|
||||
case (slot: EquipmentSlot, index: Int) =>
|
||||
slot.Equipment match {
|
||||
case Some(item) => Some(InventoryItem(item, index))
|
||||
case None => None
|
||||
}
|
||||
}.flatten.toList
|
||||
|
||||
def Inventory: GridInventory = inventory
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,16 @@ object SpawnPoint {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
|
|||
zone.GUID(obj.Telepad) match {
|
||||
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
|
||||
oldTpad.Actor ! TelepadLike.SeverLink(obj)
|
||||
case None => ;
|
||||
case _ => ;
|
||||
}
|
||||
obj.Telepad = tpad.GUID
|
||||
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class GridInventory extends Container {
|
|||
Success(collisions.toList)
|
||||
} catch {
|
||||
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 =>
|
||||
Failure(e)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ package net.psforever.objects.inventory
|
|||
* @param message the explanation of why the exception was thrown
|
||||
* @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)
|
||||
|
||||
object InventoryDisarrayException {
|
||||
|
|
@ -21,6 +25,6 @@ object InventoryDisarrayException {
|
|||
* @param message the explanation of why the exception was thrown
|
||||
* @return an `InventoryDisarrayException` object
|
||||
*/
|
||||
def apply(message: String): InventoryDisarrayException =
|
||||
InventoryDisarrayException(message, None.orNull)
|
||||
def apply(message: String, inventory: GridInventory): InventoryDisarrayException =
|
||||
InventoryDisarrayException(message, None.orNull, inventory)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,10 +652,11 @@ object ContainableBehavior {
|
|||
* 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.
|
||||
* Drop the item if:<br>
|
||||
* - the item is cavern equipment<br>
|
||||
* - the item is a `BoomerTrigger` 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
|
||||
* @return true if the item is to be dropped; false, otherwise
|
||||
*/
|
||||
|
|
@ -663,7 +664,6 @@ object ContainableBehavior {
|
|||
entry => {
|
||||
val objDef = entry.obj.Definition
|
||||
val faction = GlobalDefinitions.isFactionEquipment(objDef)
|
||||
GlobalDefinitions.isCavernEquipment(objDef) ||
|
||||
objDef == GlobalDefinitions.router_telepad ||
|
||||
entry.obj.isInstanceOf[BoomerTrigger] ||
|
||||
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import java.util.concurrent.TimeUnit
|
|||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
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.hackable.Hackable
|
||||
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.blockmap.BlockMapEntity
|
||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
|
||||
import net.psforever.types._
|
||||
import scalax.collection.{Graph, GraphEdge}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
||||
|
|
@ -88,10 +87,19 @@ class Building(
|
|||
}
|
||||
|
||||
// Get all lattice neighbours
|
||||
def Neighbours: Option[Set[Building]] = {
|
||||
def AllNeighbours: Option[Set[Building]] = {
|
||||
zone.Lattice find this match {
|
||||
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 })
|
||||
(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(
|
||||
Zone.Number,
|
||||
|
|
@ -198,25 +215,29 @@ class Building(
|
|||
hackingFaction,
|
||||
hackTime,
|
||||
if (ntuLevel > 0) Faction else PlanetSideEmpire.NEUTRAL,
|
||||
0, // unk1 Field != 0 will cause malformed packet
|
||||
None, // unk1x
|
||||
unk1 = 0, // unk1 != 0 will cause malformed packet
|
||||
unk1x = None,
|
||||
generatorState,
|
||||
spawnTubesNormal,
|
||||
forceDomeActive,
|
||||
if (generatorState != PlanetSideGeneratorState.Destroyed) latticeBenefitsValue() else 0,
|
||||
if (generatorState != PlanetSideGeneratorState.Destroyed) 48 else 0, // cavern benefit
|
||||
Nil, // unk4,
|
||||
0, // unk5
|
||||
false, // unk6
|
||||
8, // unk7 Field != 8 will cause malformed packet
|
||||
None, // unk7x
|
||||
latticeConnectedFacilityBenefits(),
|
||||
cavernBenefit,
|
||||
unk4 = Nil,
|
||||
unk5 = 0,
|
||||
unk6 = false,
|
||||
unk7 = 8, // unk7 != 8 will cause malformed packet
|
||||
unk7x = None,
|
||||
boostSpawnPain,
|
||||
boostGeneratorPain
|
||||
)
|
||||
}
|
||||
|
||||
def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = {
|
||||
if (Faction == PlanetSideEmpire.NEUTRAL) {
|
||||
def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
|
||||
val genState = Generator match {
|
||||
case Some(obj) => obj.Condition != PlanetSideGeneratorState.Destroyed
|
||||
case _ => false
|
||||
}
|
||||
if (genState || Faction == PlanetSideEmpire.NEUTRAL) {
|
||||
false
|
||||
} else {
|
||||
// Check this Building is on the lattice first
|
||||
|
|
@ -237,16 +258,16 @@ class Building(
|
|||
}
|
||||
|
||||
private def findLatticeBenefit(
|
||||
wantedBenefit: ObjectDefinition,
|
||||
wantedBenefit: LatticeBenefit,
|
||||
subGraph: Graph[Building, GraphEdge.UnDiEdge]
|
||||
): Boolean = {
|
||||
var found = false
|
||||
subGraph find this match {
|
||||
case Some(self) =>
|
||||
if (this.Definition == wantedBenefit) {
|
||||
if (this.Definition.LatticeLinkBenefit == wantedBenefit) {
|
||||
found = true
|
||||
} else {
|
||||
self pathUntil (_.Definition == wantedBenefit) match {
|
||||
self pathUntil (_.Definition.LatticeLinkBenefit == wantedBenefit) match {
|
||||
case Some(_) => found = true
|
||||
case None => ;
|
||||
}
|
||||
|
|
@ -256,54 +277,60 @@ class Building(
|
|||
found
|
||||
}
|
||||
|
||||
def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = {
|
||||
if (Faction == PlanetSideEmpire.NEUTRAL) {
|
||||
Set.empty
|
||||
def latticeConnectedFacilityBenefits(): Set[LatticeBenefit] = {
|
||||
val genState = Generator match {
|
||||
case Some(obj) => obj.Condition
|
||||
case _ => PlanetSideGeneratorState.Normal
|
||||
}
|
||||
if (genState == PlanetSideGeneratorState.Destroyed || Faction == PlanetSideEmpire.NEUTRAL) {
|
||||
Set(LatticeBenefit.None)
|
||||
} else {
|
||||
// Check this Building is on the lattice first
|
||||
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
|
||||
}
|
||||
friendlyFunctionalNeighborhood().map { _.Definition.LatticeLinkBenefit }
|
||||
}
|
||||
}
|
||||
|
||||
def latticeBenefitsValue(): Int = {
|
||||
latticeConnectedFacilityBenefits().collect {
|
||||
case GlobalDefinitions.amp_station => 1
|
||||
case GlobalDefinitions.comm_station_dsp => 2
|
||||
case GlobalDefinitions.cryo_facility => 4
|
||||
case GlobalDefinitions.comm_station => 8
|
||||
case GlobalDefinitions.tech_plant => 16
|
||||
}.sum
|
||||
def friendlyFunctionalNeighborhood(): Set[Building] = {
|
||||
var (currBuilding, newNeighbors) = Neighbours(faction).getOrElse(Set.empty[Building]).toList.splitAt(1)
|
||||
var visitedNeighbors: Set[Int] = Set(MapId)
|
||||
var friendlyNeighborhood: List[Building] = List(this)
|
||||
while (currBuilding.nonEmpty) {
|
||||
val building = currBuilding.head
|
||||
val neighborsToAdd = if (!visitedNeighbors.contains(building.MapId)
|
||||
&& (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
|
||||
|
||||
override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor
|
||||
|
|
|
|||
|
|
@ -2,10 +2,30 @@ package net.psforever.objects.serverobject.structures
|
|||
|
||||
import net.psforever.objects.{NtuContainerDefinition, SpawnPointDefinition}
|
||||
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"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
|
|||
import net.psforever.objects.{GlobalDefinitions, NtuContainer, SpawnPoint}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
|
||||
import net.psforever.types._
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
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)
|
||||
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
|
||||
|
||||
/** 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 = {
|
||||
BuildingInfoUpdateMessage(
|
||||
Zone.Number,
|
||||
MapId,
|
||||
0,
|
||||
false,
|
||||
is_hacked = false,
|
||||
PlanetSideEmpire.NEUTRAL,
|
||||
0L,
|
||||
Faction,
|
||||
0, //!! Field != 0 will cause malformed packet. See class def.
|
||||
Faction, //should be neutral in most cases
|
||||
0, //Field != 0 will cause malformed packet. See class def.
|
||||
None,
|
||||
PlanetSideGeneratorState.Normal,
|
||||
true, //TODO?
|
||||
false, //force_dome_active
|
||||
0, //lattice_benefit
|
||||
0, //cavern_benefit; !! Field > 0 will cause malformed packet. See class def.
|
||||
spawn_tubes_normal = true,
|
||||
force_dome_active = false,
|
||||
Set(LatticeBenefit.None),
|
||||
Set(CavernBenefit.None), //Field > 0 will cause malformed packet. See class def.
|
||||
Nil,
|
||||
0,
|
||||
false,
|
||||
unk6 = false,
|
||||
8, //!! Field != 8 will cause malformed packet. See class def.
|
||||
None,
|
||||
false, //boost_spawn_pain
|
||||
false //boost_generator_pain
|
||||
boost_spawn_pain = false,
|
||||
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."
|
||||
* 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";
|
||||
* `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."
|
||||
* 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";
|
||||
* `false`, otherwise
|
||||
*/
|
||||
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."
|
||||
* 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
|
||||
* Which factions 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] = {
|
||||
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
|
||||
def AllowBroadcastFor: Set[PlanetSideEmpire.Value] = passageFor
|
||||
|
||||
/**
|
||||
* Allow a faction to interact with a given warp gate as "broadcast" if it is active.
|
||||
* @param bcast the faction
|
||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||
*/
|
||||
def BroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
|
||||
(broadcast += bcast).toSet
|
||||
def AllowBroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
|
||||
AllowBroadcastFor_=(Set(bcast))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,26 +104,14 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
|
|||
* @param bcast the factions
|
||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||
*/
|
||||
def BroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
|
||||
(broadcast ++= bcast).toSet
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow a faction to interact with a given warp gate as "broadcast."
|
||||
* @param bcast the faction
|
||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||
*/
|
||||
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 AllowBroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
|
||||
val validFactions = bcast.filterNot(_ == PlanetSideEmpire.NEUTRAL)
|
||||
passageFor = if (bcast.isEmpty || validFactions.isEmpty) {
|
||||
Set(PlanetSideEmpire.NEUTRAL)
|
||||
} else {
|
||||
validFactions
|
||||
}
|
||||
AllowBroadcastFor
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
override def isOffline: Boolean = !Active
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,12 @@
|
|||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef}
|
||||
import net.psforever.objects.avatar.Certification
|
||||
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.{Default, Player}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.packet.game.ItemTransactionMessage
|
||||
import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition._
|
||||
import net.psforever.types.{ExoSuitType, TransactionType}
|
||||
import net.psforever.objects.serverobject.terminals.tabs.Tab
|
||||
import net.psforever.types.TransactionType
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
|
|
@ -37,8 +32,8 @@ import scala.collection.mutable
|
|||
class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
||||
|
||||
/** An internal object organizing the different specification options found on a terminal's UI. */
|
||||
private val tabs: mutable.HashMap[Int, OrderTerminalDefinition.Tab] =
|
||||
new mutable.HashMap[Int, OrderTerminalDefinition.Tab]()
|
||||
private val tabs: mutable.HashMap[Int, Tab] =
|
||||
new mutable.HashMap[Int, Tab]()
|
||||
|
||||
/** Disconnect the ability to return stock back to the terminal
|
||||
* 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
|
||||
|
||||
def Tab: mutable.HashMap[Int, OrderTerminalDefinition.Tab] = tabs
|
||||
def Tab: mutable.HashMap[Int, Tab] = tabs
|
||||
|
||||
def SellEquipmentByDefault: Boolean = sellEquipmentDefault
|
||||
|
||||
|
|
@ -107,337 +102,6 @@ class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
|||
}
|
||||
|
||||
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.
|
||||
* @param obj an `Amenity` object;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -253,7 +253,9 @@ case object SpikerChargeDamage extends ProjectileDamageModifiers.Mod {
|
|||
(projectile.fire_mode, projectile.profile.Charging) match {
|
||||
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
|
||||
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 _ =>
|
||||
damage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import scala.util.Try
|
|||
import akka.actor.typed
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.actors.zone.building.WarpGateLogic
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||
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.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>
|
||||
* <br>
|
||||
|
|
@ -168,6 +172,18 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
*/
|
||||
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>
|
||||
* <br>
|
||||
|
|
@ -213,8 +229,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
AssignAmenities()
|
||||
CreateSpawnGroups()
|
||||
PopulateBlockMap()
|
||||
|
||||
validate()
|
||||
|
||||
zoneInitialized.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +375,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
case (building, _) =>
|
||||
building match {
|
||||
case warpGate: WarpGate =>
|
||||
warpGate.Faction == faction || warpGate.Faction == PlanetSideEmpire.NEUTRAL || warpGate.Broadcast
|
||||
warpGate.Faction == faction || warpGate.Broadcast(faction)
|
||||
case _ =>
|
||||
building.Faction == faction
|
||||
}
|
||||
|
|
@ -570,6 +587,20 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
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] = {
|
||||
map.zipLinePaths
|
||||
}
|
||||
|
|
@ -674,15 +705,19 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
}
|
||||
|
||||
private def MakeLattice(): Unit = {
|
||||
lattice ++= map.latticeLink.map {
|
||||
case (source, target) =>
|
||||
val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match {
|
||||
case (Some(sBuilding), Some(tBuilding)) => (sBuilding, tBuilding)
|
||||
case _ =>
|
||||
throw new NoSuchElementException(s"Can't create lattice link between $source $target. Source is missing")
|
||||
}
|
||||
sourceBuilding ~ targetBuilding
|
||||
}
|
||||
lattice ++= map.latticeLink
|
||||
.filterNot {
|
||||
case (a, _) => a.contains("/") //ignore intercontinental lattice connections
|
||||
}
|
||||
.map {
|
||||
case (source, target) =>
|
||||
val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match {
|
||||
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 = {
|
||||
|
|
@ -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
|
||||
* and informs those entities that they have affected by the aforementioned damage.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import akka.actor.{Actor, ActorRef, Props}
|
|||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
|
@ -37,6 +38,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
tplayer.Zone = Zone.Nowhere
|
||||
PlayerLeave(tplayer)
|
||||
if (tplayer.VehicleSeated.isEmpty) {
|
||||
tplayer.Position = Vector3.Zero
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||
}
|
||||
sender() ! Zone.Population.PlayerHasLeft(zone, player)
|
||||
|
|
@ -70,6 +72,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
case Some(tplayer) =>
|
||||
PlayerLeave(tplayer)
|
||||
if (tplayer.VehicleSeated.isEmpty) {
|
||||
tplayer.Position = Vector3.Zero
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||
}
|
||||
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package net.psforever.objects.zones
|
|||
import akka.actor.Actor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.{Default, Vehicle}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
|
@ -52,6 +53,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
|
|||
case Some(index) =>
|
||||
vehicleList.remove(index)
|
||||
vehicle.Definition.Uninitialize(vehicle, context)
|
||||
vehicle.Position = Vector3.Zero
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
|
||||
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
|
||||
case None => ;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -556,7 +556,7 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0xd3 => game.ComponentDamageMessage.decode
|
||||
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
|
||||
case 0xd5 => game.PropertyOverrideMessage.decode
|
||||
case 0xd6 => noDecoder(WarpgateLinkOverrideMessage)
|
||||
case 0xd6 => game.WarpgateLinkOverrideMessage.decode
|
||||
case 0xd7 => noDecoder(EmpireBenefitsMessage)
|
||||
// 0xd8
|
||||
case 0xd8 => noDecoder(ForceEmpireMessage)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import scodec.Codec
|
||||
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 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)
|
||||
extends PlanetSideGamePacket {
|
||||
final case class BroadcastWarpgateUpdateMessage(
|
||||
zone_id: Int,
|
||||
building_id: Int,
|
||||
tr: Boolean,
|
||||
nc: Boolean,
|
||||
vs: Boolean
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = BroadcastWarpgateUpdateMessage
|
||||
def opcode = GamePacketOpcode.BroadcastWarpgateUpdateMessage
|
||||
def encode = BroadcastWarpgateUpdateMessage.encode(this)
|
||||
}
|
||||
|
||||
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] = (
|
||||
("zone_id" | uint16L) ::
|
||||
("building_id" | uint16L) ::
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import enumeratum.values.IntEnum
|
||||
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.codecs._
|
||||
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 collision between some parameters can occur.
|
||||
* 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>
|
||||
* <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>
|
||||
* `
|
||||
* If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.
|
||||
* @param continent_id the continent (zone)
|
||||
* @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;
|
||||
|
|
@ -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 lattice_benefit the benefits from other Lattice-linked bases does this building possess
|
||||
* @param cavern_benefit cavern benefits;
|
||||
* any non-zero value will cause the cavern module icon (yellow) to appear;
|
||||
* proper module values cause the cavern module icon to render green;
|
||||
* all benefits will report as due to a "Cavern Lock"
|
||||
* any non-zero value will cause the cavern module icon (yellow) to appear;
|
||||
* proper module values cause the cavern module icon to render green;
|
||||
* all benefits will report as due to a "Cavern Lock"
|
||||
* @param unk4 na
|
||||
* @param unk5 na
|
||||
* @param unk6 na
|
||||
|
|
@ -112,8 +92,8 @@ final case class BuildingInfoUpdateMessage(
|
|||
generator_state: PlanetSideGeneratorState.Value,
|
||||
spawn_tubes_normal: Boolean,
|
||||
force_dome_active: Boolean,
|
||||
lattice_benefit: Int,
|
||||
cavern_benefit: Int,
|
||||
lattice_benefit: Set[LatticeBenefit],
|
||||
cavern_benefit: Set[CavernBenefit],
|
||||
unk4: List[Additional2],
|
||||
unk5: Long,
|
||||
unk6: Boolean,
|
||||
|
|
@ -128,7 +108,6 @@ final case class BuildingInfoUpdateMessage(
|
|||
}
|
||||
|
||||
object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] {
|
||||
|
||||
/**
|
||||
* A `Codec` for a set of additional fields.
|
||||
*/
|
||||
|
|
@ -154,6 +133,45 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
|
|||
("unk2" | uint2L)
|
||||
).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] = (
|
||||
("continent_id" | uint16L) ::
|
||||
("building_id" | uint16L) ::
|
||||
|
|
@ -163,17 +181,17 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
|
|||
("hack_time_remaining" | uint32L) ::
|
||||
("empire_own" | PlanetSideEmpire.codec) ::
|
||||
(("unk1" | uint32L) >>:~ { unk1 =>
|
||||
conditional(unk1 != 0L, "unk1x" | additional1_codec) ::
|
||||
conditional(unk1 != 0L, codec = "unk1x" | additional1_codec) ::
|
||||
("generator_state" | PlanetSideGeneratorState.codec) ::
|
||||
("spawn_tubes_normal" | bool) ::
|
||||
("force_dome_active" | bool) ::
|
||||
("lattice_benefit" | uintL(5)) ::
|
||||
("cavern_benefit" | uintL(10)) ::
|
||||
("lattice_benefit" | latticeBenefitCodec) ::
|
||||
("cavern_benefit" | cavernBenefitCodec) ::
|
||||
("unk4" | listOfN(uint4L, additional2_codec)) ::
|
||||
("unk5" | uint32L) ::
|
||||
("unk6" | bool) ::
|
||||
(("unk7" | uint4L) >>:~ { unk7 =>
|
||||
conditional(unk7 != 8, "unk7x" | additional3_codec) ::
|
||||
conditional(unk7 != 8, codec = "unk7x" | additional3_codec) ::
|
||||
("boost_spawn_pain" | bool) ::
|
||||
("boost_generator_pain" | bool)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ final case class 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 =>
|
||||
(("has_wide_contents" | bool) >>:~ { isWide =>
|
||||
("recipient" | PacketHelpers.encodedWideStringAligned(7)) ::
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -316,6 +316,7 @@ object ObjectClass {
|
|||
final val portable_manned_turret_vs = 688
|
||||
//projectiles
|
||||
final val aphelion_plasma_cloud = 96
|
||||
final val aphelion_starfire_projectile = 108
|
||||
final val flamethrower_fire_cloud = 301
|
||||
final val hunter_seeker_missile_projectile = 405 //phoenix projectile
|
||||
final val maelstrom_grenade_damager = 464
|
||||
|
|
@ -327,6 +328,7 @@ object ObjectClass {
|
|||
final val meteor_projectile_medium = 548
|
||||
final val meteor_projectile_small = 549
|
||||
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 oicw_little_buddy = 601 //scorpion projectile's projectiles
|
||||
final val oicw_projectile = 602 //scorpion projectile
|
||||
|
|
@ -1231,6 +1233,7 @@ object ObjectClass {
|
|||
case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable")
|
||||
//projectiles
|
||||
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.meteor_common => 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_small => ConstructorData(RemoteProjectileData.codec, "meteor")
|
||||
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.oicw_little_buddy => ConstructorData(LittleBuddyProjectileData.codec, "projectile")
|
||||
case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ object RemoteProjectiles {
|
|||
final case object Meteor extends Data(0, 32)
|
||||
final case object Wasp extends Data(0, 208)
|
||||
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 Striker extends Data(26214, 134)
|
||||
final case object HunterSeeker extends Data(39577, 201)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
|
|||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
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.packet.game.DroppodError
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
|
||||
|
|
@ -14,7 +14,8 @@ import net.psforever.util.Config
|
|||
import net.psforever.zones.Zones
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Random, Success}
|
||||
|
||||
object InterstellarClusterService {
|
||||
val InterstellarClusterServiceKey: ServiceKey[Command] =
|
||||
|
|
@ -51,6 +52,8 @@ object InterstellarClusterService {
|
|||
zoneNumber: Int,
|
||||
player: Player,
|
||||
target: PlanetSideGUID,
|
||||
fromZoneNumber: Int,
|
||||
fromGateGuid: PlanetSideGUID,
|
||||
replyTo: ActorRef[SpawnPointResponse]
|
||||
) extends Command
|
||||
|
||||
|
|
@ -81,27 +84,50 @@ object InterstellarClusterService {
|
|||
replyTo: ActorRef[DroppodLaunchExchange]
|
||||
) extends Command
|
||||
|
||||
final case class CavernRotation(msg: CavernRotationService.Command) extends Command
|
||||
|
||||
trait DroppodLaunchExchange
|
||||
|
||||
final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) 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])
|
||||
extends AbstractBehavior[InterstellarClusterService.Command](context) {
|
||||
extends AbstractBehavior[InterstellarClusterService.Command](context) {
|
||||
|
||||
import InterstellarClusterService._
|
||||
|
||||
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(
|
||||
_zones.map {
|
||||
zone =>
|
||||
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}")
|
||||
(zone.id, (zoneActor, zone))
|
||||
}.toSeq: _*
|
||||
)
|
||||
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
//setup the callback upon each successful result
|
||||
val zoneLoadedList = _zones.map { _.ZoneInitialized() }
|
||||
val continentLinkFunc: ()=>Unit = MakeIntercontinentalLattice(
|
||||
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 {
|
||||
case (_, (_, zone: Zone)) => zone
|
||||
|
|
@ -109,8 +135,21 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
|||
|
||||
override def onMessage(msg: Command): Behavior[Command] = {
|
||||
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) =>
|
||||
replyTo ! PlayersResponse(zones.flatMap(_.Players).toSeq)
|
||||
|
||||
case FindZoneActor(predicate, replyTo) =>
|
||||
replyTo ! ZoneActorResponse(
|
||||
zoneActors.collectFirst {
|
||||
|
|
@ -170,18 +209,6 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
|||
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
|
||||
val response = zones.find(_.Number == zoneNumber) match {
|
||||
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 {
|
||||
case Some((_, spawnPoints)) if spawnPoints.nonEmpty =>
|
||||
Some((zone, Random.shuffle(spawnPoints.toList).head))
|
||||
|
|
@ -194,9 +221,10 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
|||
}
|
||||
replyTo ! SpawnPointResponse(response)
|
||||
|
||||
case GetSpawnPoint(zoneNumber, player, target, replyTo) =>
|
||||
case GetSpawnPoint(zoneNumber, player, target, fromZoneNumber, fromOriginGuid, replyTo) =>
|
||||
zones.find(_.Number == zoneNumber) match {
|
||||
case Some(zone) =>
|
||||
//found target zone; find a spawn point in target zone
|
||||
zone.findSpawns(player.Faction, SpawnGroup.values).find {
|
||||
case (spawn: Building, spawnPoints) =>
|
||||
spawn.MapId == target.guid || spawnPoints.exists(_.GUID == target)
|
||||
|
|
@ -205,12 +233,32 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
|||
case _ => false
|
||||
} match {
|
||||
case Some((_, spawnPoints)) =>
|
||||
//spawn point selected
|
||||
replyTo ! SpawnPointResponse(Some(zone, Random.shuffle(spawnPoints.toList).head))
|
||||
case _ =>
|
||||
//no spawn point found
|
||||
replyTo ! SpawnPointResponse(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) =>
|
||||
|
|
@ -241,9 +289,102 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
|||
case None =>
|
||||
replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None)
|
||||
}
|
||||
}
|
||||
|
||||
case CavernRotation(rotationMsg) =>
|
||||
cavernRotation match {
|
||||
case Some(rotation) => rotation ! rotationMsg
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ class GalaxyService extends Actor {
|
|||
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) =>
|
||||
GalaxyEvents.publish(
|
||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg))
|
||||
|
|
@ -53,6 +61,27 @@ class GalaxyService extends Actor {
|
|||
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 _ => ;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package net.psforever.services.galaxy
|
|||
|
||||
import net.psforever.objects.Vehicle
|
||||
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.types.PlanetSideGUID
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action)
|
||||
|
||||
|
|
@ -25,4 +27,17 @@ object GalaxyAction {
|
|||
vehicle_to_delete: PlanetSideGUID,
|
||||
manifest: VehicleManifest
|
||||
) 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package net.psforever.services.galaxy
|
|||
|
||||
import net.psforever.objects.Vehicle
|
||||
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.types.PlanetSideGUID
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||
import net.psforever.services.GenericEventBusMsg
|
||||
|
||||
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
|
||||
|
|
@ -25,4 +26,17 @@ object GalaxyResponse {
|
|||
vehicle_to_delete: PlanetSideGUID,
|
||||
manifest: VehicleManifest
|
||||
) 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
|
||||
}
|
||||
|
|
|
|||
61
src/main/scala/net/psforever/types/CaptureBenefits.scala
Normal file
61
src/main/scala/net/psforever/types/CaptureBenefits.scala
Normal 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)
|
||||
}
|
||||
|
|
@ -156,7 +156,9 @@ case class GameConfig(
|
|||
newAvatar: NewAvatar,
|
||||
hart: HartConfig,
|
||||
sharedMaxCooldown: Boolean,
|
||||
baseCertifications: Seq[Certification]
|
||||
baseCertifications: Seq[Certification],
|
||||
warpGates: WarpGateConfig,
|
||||
cavernRotation: CavernRotationConfig
|
||||
)
|
||||
|
||||
case class NewAvatar(
|
||||
|
|
@ -190,3 +192,15 @@ case class SentryConfig(
|
|||
enable: Boolean,
|
||||
dsn: String
|
||||
)
|
||||
|
||||
case class WarpGateConfig(
|
||||
defaultToSanctuaryDestination: Boolean,
|
||||
broadcastBetweenConflictedFactions: Boolean
|
||||
)
|
||||
|
||||
case class CavernRotationConfig(
|
||||
hoursBetweenRotation: Float,
|
||||
simultaneousUnlockedZones: Int,
|
||||
enhancedRotationOrder: Seq[Int],
|
||||
forceRotationImmediately: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -228,7 +228,8 @@ object Zones {
|
|||
}
|
||||
.map {
|
||||
case (info, data, zplData) =>
|
||||
val zoneMap = new ZoneMap(info.value)
|
||||
val mapid = info.value
|
||||
val zoneMap = new ZoneMap(mapid)
|
||||
|
||||
zoneMap.checksum = info.checksum
|
||||
zoneMap.scale = info.scale
|
||||
|
|
@ -298,7 +299,6 @@ object Zones {
|
|||
if (facilityTypes.contains(structure.objectType)) {
|
||||
//major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons
|
||||
val buildingGuid = structure.guid
|
||||
val terminalGuid = buildingGuid + 1
|
||||
zoneMap.addLocalObject(
|
||||
buildingGuid + 1,
|
||||
ProximityTerminal.Constructor(
|
||||
|
|
@ -346,13 +346,12 @@ object Zones {
|
|||
zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
|
||||
}
|
||||
|
||||
lattice.asObject.get(info.value).foreach { obj =>
|
||||
lattice.asObject.get(mapid).foreach { obj =>
|
||||
obj.asArray.get.foreach { entry =>
|
||||
val arr = entry.asArray.get
|
||||
zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get)
|
||||
}
|
||||
}
|
||||
|
||||
zoneMap
|
||||
}
|
||||
.seq
|
||||
|
|
@ -654,6 +653,12 @@ object Zones {
|
|||
}
|
||||
|
||||
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 =
|
||||
try {
|
||||
val res = Source.fromResource("guid-pools/default.json")
|
||||
|
|
@ -676,7 +681,7 @@ object Zones {
|
|||
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)
|
||||
|
||||
override def SetupNumberPools() : Unit = addPoolsFunc()
|
||||
|
|
@ -690,31 +695,121 @@ object Zones {
|
|||
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 {
|
||||
case "home1" =>
|
||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.NC)
|
||||
case "home2" =>
|
||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.TR)
|
||||
case "home3" =>
|
||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.VS)
|
||||
case zoneid if zoneid.startsWith("c") =>
|
||||
this.map.cavern = true
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
// Set up warp gate factions aka "sanctuary link". Those names make no sense anymore, don't even ask.
|
||||
this.Buildings.foreach {
|
||||
case (_, building) if building.Name.startsWith("WG") =>
|
||||
building.Name match {
|
||||
case "WG_Amerish_to_Solsar" | "WG_Esamir_to_VSSanc" => building.Faction = PlanetSideEmpire.NC
|
||||
case "WG_Hossin_to_VSSanc" | "WG_Solsar_to_Amerish" => building.Faction = PlanetSideEmpire.TR
|
||||
case "WG_Ceryshen_to_Hossin" | "WG_Forseral_to_Solsar" => building.Faction = PlanetSideEmpire.VS
|
||||
case _ => ;
|
||||
case "z1" =>
|
||||
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Solsar_to_Amerish", PlanetSideEmpire.TR)
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z2" =>
|
||||
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Hossin_to_VSSanc", PlanetSideEmpire.TR)
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z3" =>
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z4" =>
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z5" =>
|
||||
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Forseral_to_Solsar", PlanetSideEmpire.VS)
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z6" =>
|
||||
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Ceryshen_to_Hossin", PlanetSideEmpire.VS)
|
||||
deactivateGeoWarpGateOnContinent(bldgs)
|
||||
case "z7" =>
|
||||
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Esamir_to_VSSanc", PlanetSideEmpire.NC)
|
||||
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 _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 =>
|
||||
wg.Active = true
|
||||
wg.Faction = PlanetSideEmpire.NEUTRAL
|
||||
wg.Broadcast = true
|
||||
case geowarp: WarpGate
|
||||
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
|
||||
geowarp.Faction = PlanetSideEmpire.NEUTRAL
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package game
|
|||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState}
|
||||
import net.psforever.types.{CavernBenefit, LatticeBenefit, PlanetSideEmpire, PlanetSideGeneratorState}
|
||||
import scodec.bits._
|
||||
|
||||
class BuildingInfoUpdateMessageTest extends Specification {
|
||||
|
|
@ -26,7 +26,7 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
|||
spawn_tubes_normal,
|
||||
force_dome_active,
|
||||
lattice_benefit,
|
||||
unk3,
|
||||
cavern_benefit,
|
||||
unk4,
|
||||
unk5,
|
||||
unk6,
|
||||
|
|
@ -43,18 +43,18 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
|||
hack_time_remaining mustEqual 0
|
||||
empire_own mustEqual PlanetSideEmpire.NC
|
||||
unk1 mustEqual 0
|
||||
unk1x mustEqual None
|
||||
unk1x.isEmpty mustEqual true
|
||||
generator_state mustEqual PlanetSideGeneratorState.Normal
|
||||
spawn_tubes_normal mustEqual true
|
||||
force_dome_active mustEqual false
|
||||
lattice_benefit mustEqual 28
|
||||
unk3 mustEqual 0
|
||||
lattice_benefit mustEqual Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory)
|
||||
cavern_benefit mustEqual Set(CavernBenefit.None)
|
||||
unk4.size mustEqual 0
|
||||
unk4.isEmpty mustEqual true
|
||||
unk5 mustEqual 0
|
||||
unk6 mustEqual false
|
||||
unk7 mustEqual 8
|
||||
unk7x mustEqual None
|
||||
unk7x.isEmpty mustEqual true
|
||||
boost_spawn_pain mustEqual false
|
||||
boost_generator_pain mustEqual false
|
||||
case _ =>
|
||||
|
|
@ -76,8 +76,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
|||
PlanetSideGeneratorState.Normal,
|
||||
true,
|
||||
false,
|
||||
28,
|
||||
0,
|
||||
Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory),
|
||||
Set(CavernBenefit.None),
|
||||
Nil,
|
||||
0,
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -300,11 +300,8 @@ class DamageCalculationsTests extends Specification {
|
|||
),
|
||||
Vector3(15, 0, 0)
|
||||
)
|
||||
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||
val calcDam = minDamageBase + math.floor(
|
||||
chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
|
||||
)
|
||||
damage mustEqual calcDam
|
||||
/*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||
ok
|
||||
}
|
||||
|
||||
"charge (full)" in {
|
||||
|
|
@ -318,9 +315,8 @@ class DamageCalculationsTests extends Specification {
|
|||
),
|
||||
Vector3(15, 0, 0)
|
||||
)
|
||||
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||
val calcDam = minDamageBase + chargeBaseDamage
|
||||
damage mustEqual calcDam
|
||||
/*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||
ok
|
||||
}
|
||||
|
||||
val flak_weapon = Tool(GlobalDefinitions.trhev_burster)
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ class InventoryTest extends Specification {
|
|||
|
||||
"InventoryDisarrayException" should {
|
||||
"construct" in {
|
||||
InventoryDisarrayException("slot out of bounds")
|
||||
InventoryDisarrayException("slot out of bounds", GridInventory())
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (with Throwable)" in {
|
||||
InventoryDisarrayException("slot out of bounds", new Throwable())
|
||||
InventoryDisarrayException("slot out of bounds", new Throwable(), GridInventory())
|
||||
ok
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue