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.galaxy.GalaxyService
|
||||||
import net.psforever.services.properties.PropertyOverrideManager
|
import net.psforever.services.properties.PropertyOverrideManager
|
||||||
import net.psforever.services.teamwork.SquadService
|
import net.psforever.services.teamwork.SquadService
|
||||||
import net.psforever.services.{InterstellarClusterService, ServiceManager}
|
import net.psforever.services.{CavernRotationService, InterstellarClusterService, ServiceManager}
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.Config
|
||||||
import net.psforever.zones.Zones
|
import net.psforever.zones.Zones
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
@ -120,10 +120,6 @@ object Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
||||||
|
|
||||||
system.spawn(ChatService(), ChatService.ChatServiceKey.id)
|
|
||||||
system.spawn(InterstellarClusterService(zones), InterstellarClusterService.InterstellarClusterServiceKey.id)
|
|
||||||
|
|
||||||
val serviceManager = ServiceManager.boot
|
val serviceManager = ServiceManager.boot
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
|
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy")
|
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy")
|
||||||
|
|
@ -132,6 +128,10 @@ object Server {
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager")
|
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager")
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart")
|
serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart")
|
||||||
|
|
||||||
|
system.spawn(CavernRotationService(), CavernRotationService.CavernRotationServiceKey.id)
|
||||||
|
system.spawn(InterstellarClusterService(zones), InterstellarClusterService.InterstellarClusterServiceKey.id)
|
||||||
|
system.spawn(ChatService(), ChatService.ChatServiceKey.id)
|
||||||
|
|
||||||
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket")
|
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket")
|
||||||
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")
|
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,43 @@ game {
|
||||||
standard_armor,
|
standard_armor,
|
||||||
agile_armor
|
agile_armor
|
||||||
]
|
]
|
||||||
|
|
||||||
|
warp-gates {
|
||||||
|
# When a gating fails, fall back to sanctuary rather than stay in the same zone
|
||||||
|
default-to-sanctuary-destination = yes
|
||||||
|
|
||||||
|
# When a facility next to a warp gate is captured by one faction,
|
||||||
|
# if the facility on the other side of the intercontinental gate pair is owned by a different faction,
|
||||||
|
# that gate pair becomes a broadcast warp gate for those factions
|
||||||
|
broadcast-between-conflicted-factions = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
cavern-rotation = {
|
||||||
|
# The number of hours between any given cavern locking and another cavern unlocking,
|
||||||
|
# not the total number of hours between a single cavern locking and unlocking.
|
||||||
|
hours-between-rotation = 3
|
||||||
|
|
||||||
|
# How many caverns are unlocked at once during rotations
|
||||||
|
# Pay attention to the logs that a corresponding combinational existence found in zonemaps/lattice.json
|
||||||
|
# Examples:
|
||||||
|
# [a,b,c] with 1 requires 'caverns-a' 'caverns-b' 'caverns-c'
|
||||||
|
# [a,b,c] with 2 requires 'caverns-a-b' 'caverns-b-c' 'caverns-a-c'
|
||||||
|
# [a,b,c] with 3 requires 'caverns-a-b-c'
|
||||||
|
# [a,b,c,d] with 3 requires 'caverns-a-b-c' 'caverns-b-c-d' 'caverns-a-c-d' 'caverns-a-b-d'
|
||||||
|
simultaneous-unlocked-zones = 2
|
||||||
|
|
||||||
|
# A list of zone numbers that correspond to the caverns, in a continuous order
|
||||||
|
# in which the caverns are locked and are unlocked.
|
||||||
|
# When left empty, the order of the caverns is traversed as-is provided
|
||||||
|
# For example, for cavern zones with number [23, 24, 25, 26, 27, 28],
|
||||||
|
# the order [23, 24, 26, 23, 24, 25, 26] would eliminate #27 and #28 from the rotation
|
||||||
|
enhanced-rotation-order = [23, 24, 25, 26, 27, 28]
|
||||||
|
|
||||||
|
# If a cavern rotation is forced by the system,
|
||||||
|
# the system will attempt to advance only to the first possible closing warning message at five minutes
|
||||||
|
# When set, however, the next zone unlock is carried out regardless of the amount of time remaining
|
||||||
|
force-rotation-immediately = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anti-cheat {
|
anti-cheat {
|
||||||
|
|
|
||||||
|
|
@ -1027,7 +1027,7 @@
|
||||||
"Map99_Gate_Three"
|
"Map99_Gate_Three"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg01": [
|
"ugd01": [
|
||||||
[
|
[
|
||||||
"N_Redoubt",
|
"N_Redoubt",
|
||||||
"N_ATPlant"
|
"N_ATPlant"
|
||||||
|
|
@ -1077,7 +1077,7 @@
|
||||||
"S_ATPlant"
|
"S_ATPlant"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg02": [
|
"ugd02": [
|
||||||
[
|
[
|
||||||
"N_Redoubt",
|
"N_Redoubt",
|
||||||
"N_ATPlant"
|
"N_ATPlant"
|
||||||
|
|
@ -1127,7 +1127,7 @@
|
||||||
"S_ATPlant"
|
"S_ATPlant"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg03": [
|
"ugd03": [
|
||||||
[
|
[
|
||||||
"NW_Redoubt",
|
"NW_Redoubt",
|
||||||
"NW_ATPlant"
|
"NW_ATPlant"
|
||||||
|
|
@ -1177,7 +1177,7 @@
|
||||||
"SE_ATPlant"
|
"SE_ATPlant"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg04": [
|
"ugd04": [
|
||||||
[
|
[
|
||||||
"N_Redoubt",
|
"N_Redoubt",
|
||||||
"N_ATPlant"
|
"N_ATPlant"
|
||||||
|
|
@ -1227,7 +1227,7 @@
|
||||||
"S_ATPlant"
|
"S_ATPlant"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg05": [
|
"ugd05": [
|
||||||
[
|
[
|
||||||
"NW_Redoubt",
|
"NW_Redoubt",
|
||||||
"NW_ATPlant"
|
"NW_ATPlant"
|
||||||
|
|
@ -1281,7 +1281,7 @@
|
||||||
"SE_ATPlant"
|
"SE_ATPlant"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"udg06": [
|
"ugd06": [
|
||||||
[
|
[
|
||||||
"N_Redoubt",
|
"N_Redoubt",
|
||||||
"N_ATPlant"
|
"N_ATPlant"
|
||||||
|
|
@ -1330,5 +1330,423 @@
|
||||||
"GW_Cavern6_S",
|
"GW_Cavern6_S",
|
||||||
"S_ATPlant"
|
"S_ATPlant"
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
"intercontinental": [
|
||||||
|
[
|
||||||
|
"z1/WG_Solsar_to_Amerish",
|
||||||
|
"home2/WG_TRSanc_to_Cyssor"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z1/WG_Solsar_to_Cyssor",
|
||||||
|
"z3/WG_Cyssor_to_NCSanc"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z1/WG_Solsar_to_Hossin",
|
||||||
|
"z6/WG_Ceryshen_to_Forseral"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z1/WG_Solsar_to_Forseral",
|
||||||
|
"i3/Map97_Gate_One"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z2/WG_Hossin_to_VSSanc",
|
||||||
|
"home2/WG_TRSanc_to_Forseral"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z2/WG_Hossin_to_Solsar",
|
||||||
|
"z4/WG_Ishundar_to_Searhus"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z2/WG_Hossin_to_Ceryshen",
|
||||||
|
"z10/WG_Amerish_to_Ceryshen"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z2/WG_Hossin_to_Oshur",
|
||||||
|
"z9/WG_Searhus_to_Ishundar"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z3/WG_Cyssor_to_Searhus",
|
||||||
|
"z7/WG_Esamir_to_Oshur"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z3/WG_Cyssor_to_Solsar",
|
||||||
|
"z4/WG_Ishundar_to_VSSanc"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z3/WG_Cyssor_to_TRSanc",
|
||||||
|
"z6/WG_Ceryshen_to_Amerish"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z4/WG_Ishundar_to_TRSanc",
|
||||||
|
"z7/WG_Esamir_to_Searhus"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z4/WG_Ishundar_to_Ceryshen",
|
||||||
|
"z5/WG_Forseral_to_Ceryshen"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z5/WG_Forseral_to_TRSanc",
|
||||||
|
"i2/Map98_Gate_One"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z5/WG_Forseral_to_Solsar",
|
||||||
|
"home3/WG_VSSanc_to_Hossin"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z5/WG_Forseral_to_Oshur",
|
||||||
|
"z7/WG_Esamir_to_NCSanc"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z6/WG_Ceryshen_to_Ishundar",
|
||||||
|
"z9/WG_Searhus_to_Esamir"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z6/WG_Ceryshen_to_Hossin",
|
||||||
|
"home3/WG_VSSanc_to_Esamir"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z7/WG_Esamir_to_VSSanc",
|
||||||
|
"home1/WG_NCSanc_to_Esamir"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z9/WG_Searhus_to_Cyssor",
|
||||||
|
"z10/WG_Amerish_to_NCSanc"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z10/WG_Amerish_to_Solsar",
|
||||||
|
"home1/WG_NCSanc_to_Amerish"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"z10/WG_Amerish_to_Oshur",
|
||||||
|
"i1/Map99_Gate_One"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"i1/Map99_Gate_Two",
|
||||||
|
"i3/Map97_Gate_Three"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"i2/Map98_Gate_Three",
|
||||||
|
"i3/Map97_Gate_Two"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"i4/Map96_Gate_One",
|
||||||
|
"i2/Map98_Gate_Two"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"i4/Map96_Gate_Two",
|
||||||
|
"i1/Map99_Gate_Three"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c1": [
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_N",
|
||||||
|
"z10/GW_Amerish_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_S",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_E",
|
||||||
|
"z1/GW_Solsar_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_W",
|
||||||
|
"z4/GW_Ishundar_N"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c2": [
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_N",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_S",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_E",
|
||||||
|
"z6/GW_Ceryshen_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_W",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c3": [
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_N",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_S",
|
||||||
|
"z2/GW_Hossin_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_E",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_W",
|
||||||
|
"z9/GW_Searhus_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c4": [
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_N",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_S",
|
||||||
|
"z10/GW_Amerish_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_E",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_W",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c5": [
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_N",
|
||||||
|
"z2/GW_Hossin_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_S",
|
||||||
|
"z1/GW_Solsar_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_E",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_W",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c6": [
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_N",
|
||||||
|
"z5/GW_Forseral_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_S",
|
||||||
|
"z2/GW_Hossin_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_E",
|
||||||
|
"z4/GW_Ishundar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_W",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c1-c2": [
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_N",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_S",
|
||||||
|
"z6/GW_Ceryshen_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_E",
|
||||||
|
"z1/GW_Solsar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_W",
|
||||||
|
"z3/GW_Cyssor_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_N",
|
||||||
|
"z4/GW_Ishundar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_S",
|
||||||
|
"z2/GW_Hossin_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_E",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_W",
|
||||||
|
"z10/GW_Amerish_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c2-c3": [
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_N",
|
||||||
|
"z7/GW_Esamir_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_S",
|
||||||
|
"z4/GW_Ishundar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_E",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c2/GW_Cavern2_W",
|
||||||
|
"z2/GW_Hossin_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_N",
|
||||||
|
"z10/GW_Amerish_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_S",
|
||||||
|
"z1/GW_Solsar_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_E",
|
||||||
|
"z6/GW_Ceryshen_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_W",
|
||||||
|
"z5/GW_Forseral_S"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c3-c4": [
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_N",
|
||||||
|
"z6/GW_Ceryshen_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_S",
|
||||||
|
"z4/GW_Ishundar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_E",
|
||||||
|
"z10/GW_Amerish_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c3/GW_Cavern3_W",
|
||||||
|
"z2/GW_Hossin_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_N",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_S",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_E",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_W",
|
||||||
|
"z3/GW_Cyssor_N"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c4-c5": [
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_N",
|
||||||
|
"z4/GW_Ishundar_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_S",
|
||||||
|
"z1/GW_Solsar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_E",
|
||||||
|
"z3/GW_Cyssor_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c4/GW_Cavern4_W",
|
||||||
|
"z2/GW_Hossin_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_N",
|
||||||
|
"z10/GW_Amerish_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_S",
|
||||||
|
"z6/GW_Ceryshen_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_E",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_W",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c5-c6": [
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_N",
|
||||||
|
"z10/GW_Amerish_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_S",
|
||||||
|
"z7/GW_Esamir_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_E",
|
||||||
|
"z2/GW_Hossin_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c5/GW_Cavern5_W",
|
||||||
|
"z1/GW_Solsar_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_N",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_S",
|
||||||
|
"z3/GW_Cyssor_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_E",
|
||||||
|
"z6/GW_Ceryshen_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_W",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"caverns-c1-c6": [
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_N",
|
||||||
|
"z3/GW_Cyssor_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_S",
|
||||||
|
"z1/GW_Solsar_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_E",
|
||||||
|
"z6/GW_Ceryshen_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c1/GW_Cavern1_W",
|
||||||
|
"z5/GW_Forseral_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_N",
|
||||||
|
"z2/GW_Hossin_S"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_S",
|
||||||
|
"z10/GW_Amerish_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_E",
|
||||||
|
"z9/GW_Searhus_N"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c6/GW_Cavern6_W",
|
||||||
|
"z4/GW_Ishundar_N"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cos
|
||||||
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
|
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
|
||||||
import net.psforever.objects.{Default, Player, Session}
|
import net.psforever.objects.{Default, Player, Session}
|
||||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||||
import net.psforever.objects.serverobject.structures.Building
|
import net.psforever.objects.serverobject.structures.{Amenity, Building}
|
||||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
|
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
|
||||||
import net.psforever.objects.zones.Zoning
|
import net.psforever.objects.zones.Zoning
|
||||||
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
|
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
|
||||||
|
|
@ -18,9 +18,12 @@ import net.psforever.util.{Config, PointOfInterest}
|
||||||
import net.psforever.zones.Zones
|
import net.psforever.zones.Zones
|
||||||
import net.psforever.services.chat.ChatService
|
import net.psforever.services.chat.ChatService
|
||||||
import net.psforever.services.chat.ChatService.ChatChannel
|
import net.psforever.services.chat.ChatService.ChatChannel
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContextExecutor
|
import scala.concurrent.ExecutionContextExecutor
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
|
import net.psforever.services.{CavernRotationService, InterstellarClusterService}
|
||||||
|
import net.psforever.types.ChatMessageType.UNK_229
|
||||||
|
|
||||||
object ChatActor {
|
object ChatActor {
|
||||||
def apply(
|
def apply(
|
||||||
|
|
@ -44,6 +47,63 @@ object ChatActor {
|
||||||
|
|
||||||
private case class ListingResponse(listing: Receptionist.Listing) extends Command
|
private case class ListingResponse(listing: Receptionist.Listing) extends Command
|
||||||
private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
|
private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a provided number of facility nanite transfer unit resource silos,
|
||||||
|
* charge the facility's silo with an expected amount of nanite transfer units.
|
||||||
|
* @see `Amenity`
|
||||||
|
* @see `ChatMsg`
|
||||||
|
* @see `ResourceSilo`
|
||||||
|
* @see `ResourceSilo.UpdateChargeLevel`
|
||||||
|
* @see `SessionActor.Command`
|
||||||
|
* @see `SessionActor.SendResponse`
|
||||||
|
* @param session messaging reference back tothe target session
|
||||||
|
* @param resources the optional number of resources to set to each silo;
|
||||||
|
* different values provide different resources as indicated below;
|
||||||
|
* an undefined value also has a condition
|
||||||
|
* @param silos where to deposit the resources
|
||||||
|
* @param debugContent something for log output context
|
||||||
|
*/
|
||||||
|
private def setBaseResources(
|
||||||
|
session: ActorRef[SessionActor.Command],
|
||||||
|
resources: Option[Int],
|
||||||
|
silos: Iterable[Amenity],
|
||||||
|
debugContent: String
|
||||||
|
): Unit = {
|
||||||
|
if (silos.isEmpty) {
|
||||||
|
session ! SessionActor.SendResponse(
|
||||||
|
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $debugContent", None)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
resources match {
|
||||||
|
// x = n0% of maximum capacitance
|
||||||
|
case Some(value) if value > -1 && value < 11 =>
|
||||||
|
silos.collect {
|
||||||
|
case silo: ResourceSilo =>
|
||||||
|
silo.Actor ! ResourceSilo.UpdateChargeLevel(
|
||||||
|
value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// capacitance set to x (where x > 10) exactly, within limits
|
||||||
|
case Some(value) =>
|
||||||
|
silos.collect {
|
||||||
|
case silo: ResourceSilo =>
|
||||||
|
silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
// x >= n0% of maximum capacitance and x <= maximum capacitance
|
||||||
|
val rand = new scala.util.Random
|
||||||
|
silos.collect {
|
||||||
|
case silo: ResourceSilo =>
|
||||||
|
val a = 7
|
||||||
|
val b = 10 - a
|
||||||
|
val tenth = silo.MaxNtuCapacitor * 0.1f
|
||||||
|
silo.Actor ! ResourceSilo.UpdateChargeLevel(
|
||||||
|
a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatActor(
|
class ChatActor(
|
||||||
|
|
@ -57,14 +117,15 @@ class ChatActor(
|
||||||
|
|
||||||
implicit val ec: ExecutionContextExecutor = context.executionContext
|
implicit val ec: ExecutionContextExecutor = context.executionContext
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
var channels: List[ChatChannel] = List()
|
var channels: List[ChatChannel] = List()
|
||||||
var session: Option[Session] = None
|
var session: Option[Session] = None
|
||||||
var chatService: Option[ActorRef[ChatService.Command]] = None
|
var chatService: Option[ActorRef[ChatService.Command]] = None
|
||||||
var silenceTimer: Cancellable = Default.Cancellable
|
var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
||||||
|
var silenceTimer: Cancellable = Default.Cancellable
|
||||||
|
|
||||||
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
|
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
|
||||||
case ChatService.MessageResponse(session, message, channel) => IncomingMessage(session, message, channel)
|
case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.system.receptionist ! Receptionist.Find(
|
context.system.receptionist ! Receptionist.Find(
|
||||||
|
|
@ -72,43 +133,63 @@ class ChatActor(
|
||||||
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
context.system.receptionist ! Receptionist.Find(
|
||||||
|
InterstellarClusterService.InterstellarClusterServiceKey,
|
||||||
|
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||||
|
)
|
||||||
|
|
||||||
def start(): Behavior[Command] = {
|
def start(): Behavior[Command] = {
|
||||||
Behaviors
|
Behaviors
|
||||||
.receiveMessage[Command] {
|
.receiveMessage[Command] {
|
||||||
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
|
case ListingResponse(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
|
||||||
chatService = Some(listings.head)
|
listings.headOption match {
|
||||||
channels ++= List(ChatChannel.Default())
|
case Some(ref) =>
|
||||||
postStartBehaviour()
|
cluster = Some(ref)
|
||||||
|
postStartBehaviour()
|
||||||
|
case None =>
|
||||||
|
context.system.receptionist ! Receptionist.Find(
|
||||||
|
InterstellarClusterService.InterstellarClusterServiceKey,
|
||||||
|
context.messageAdapter[Receptionist.Listing](ListingResponse)
|
||||||
|
)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
|
||||||
case SetSession(newSession) =>
|
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
|
||||||
session = Some(newSession)
|
chatService = Some(listings.head)
|
||||||
postStartBehaviour()
|
channels ++= List(ChatChannel.Default())
|
||||||
|
postStartBehaviour()
|
||||||
|
|
||||||
case other =>
|
case SetSession(newSession) =>
|
||||||
buffer.stash(other)
|
session = Some(newSession)
|
||||||
Behaviors.same
|
postStartBehaviour()
|
||||||
}
|
|
||||||
|
|
||||||
|
case other =>
|
||||||
|
buffer.stash(other)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def postStartBehaviour(): Behavior[Command] = {
|
def postStartBehaviour(): Behavior[Command] = {
|
||||||
(session, chatService) match {
|
(session, chatService, cluster) match {
|
||||||
case (Some(session), Some(chatService)) if session.player != null =>
|
case (Some(_session), Some(_chatService), Some(_cluster)) if _session.player != null =>
|
||||||
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, ChatChannel.Default())
|
_chatService ! ChatService.JoinChannel(chatServiceAdapter, _session, ChatChannel.Default())
|
||||||
buffer.unstashAll(active(session, chatService))
|
buffer.unstashAll(active(_session, _chatService, _cluster))
|
||||||
case _ =>
|
case _ =>
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def active(session: Session, chatService: ActorRef[ChatService.Command]): Behavior[Command] = {
|
def active(
|
||||||
|
session: Session,
|
||||||
|
chatService: ActorRef[ChatService.Command],
|
||||||
|
cluster: ActorRef[InterstellarClusterService.Command]
|
||||||
|
): Behavior[Command] = {
|
||||||
import ChatMessageType._
|
import ChatMessageType._
|
||||||
|
|
||||||
Behaviors
|
Behaviors
|
||||||
.receiveMessagePartial[Command] {
|
.receiveMessagePartial[Command] {
|
||||||
case SetSession(newSession) =>
|
case SetSession(newSession) =>
|
||||||
active(newSession, chatService)
|
active(newSession, chatService,cluster)
|
||||||
|
|
||||||
case JoinChannel(channel) =>
|
case JoinChannel(channel) =>
|
||||||
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
|
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
|
||||||
|
|
@ -275,10 +356,63 @@ class ChatActor(
|
||||||
}
|
}
|
||||||
sessionActor ! SessionActor.SendResponse(message)
|
sessionActor ! SessionActor.SendResponse(message)
|
||||||
|
|
||||||
|
case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
|
||||||
|
val buffer = contents.toLowerCase.split("\\s+")
|
||||||
|
val customNtuValue = buffer.lift(1) match {
|
||||||
|
case Some(x) if x.toIntOption.nonEmpty => Some(x.toInt)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
val silos = {
|
||||||
|
val position = session.player.Position
|
||||||
|
session.zone.Buildings.values
|
||||||
|
.filter { building =>
|
||||||
|
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
|
||||||
|
Vector3.DistanceSquared(building.Position, position) < soi2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
|
||||||
|
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent="")
|
||||||
|
|
||||||
|
case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
|
||||||
|
val buffer = contents.toLowerCase.split("\\s+")
|
||||||
|
val (zoneOpt, lockVal) = (buffer.lift(1), buffer.lift(2)) match {
|
||||||
|
case (Some(x), Some(y)) =>
|
||||||
|
val zone = if (x.toIntOption.nonEmpty) {
|
||||||
|
val xInt = x.toInt
|
||||||
|
Zones.zones.find(_.Number == xInt)
|
||||||
|
} else {
|
||||||
|
Zones.zones.find(z => z.id.equals(x))
|
||||||
|
}
|
||||||
|
val value = if (y.toIntOption.nonEmpty && y.toInt == 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
(zone, Some(value))
|
||||||
|
case _ =>
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
(zoneOpt, lockVal) match {
|
||||||
|
case (Some(zone), Some(lock)) if zone.id.startsWith("c") =>
|
||||||
|
//caverns must be rotated in an order
|
||||||
|
if (lock == 0) {
|
||||||
|
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneUnlock(zone.id))
|
||||||
|
} else {
|
||||||
|
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneLock(zone.id))
|
||||||
|
}
|
||||||
|
case (Some(zone), Some(lock)) =>
|
||||||
|
//normal zones can lock when all facilities and towers on it belong to the same faction
|
||||||
|
//normal zones can lock when ???
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
case (U_CMT_ZONEROTATE, _, contents) if gmCommandAllowed =>
|
||||||
|
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
|
||||||
|
|
||||||
/** Messages starting with ! are custom chat commands */
|
/** Messages starting with ! are custom chat commands */
|
||||||
case (messageType, recipient, contents) if contents.startsWith("!") =>
|
case (messageType, recipient, contents) if contents.startsWith("!") =>
|
||||||
(messageType, recipient, contents) match {
|
(messageType, recipient, contents) match {
|
||||||
case (_, _, contents) if contents.startsWith("!whitetext ") && session.account.gm =>
|
case (_, _, _contents) if _contents.startsWith("!whitetext ") && session.account.gm =>
|
||||||
chatService ! ChatService.Message(
|
chatService ! ChatService.Message(
|
||||||
session,
|
session,
|
||||||
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
||||||
|
|
@ -293,8 +427,8 @@ class ChatActor(
|
||||||
log.info(loc)
|
log.info(loc)
|
||||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
|
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
|
||||||
|
|
||||||
case (_, _, contents) if contents.startsWith("!list") =>
|
case (_, _, content) if content.startsWith("!list") =>
|
||||||
val zone = contents.split(" ").lift(1) match {
|
val zone = content.split(" ").lift(1) match {
|
||||||
case None =>
|
case None =>
|
||||||
Some(session.zone)
|
Some(session.zone)
|
||||||
case Some(id) =>
|
case Some(id) =>
|
||||||
|
|
@ -302,7 +436,7 @@ class ChatActor(
|
||||||
}
|
}
|
||||||
|
|
||||||
zone match {
|
zone match {
|
||||||
case Some(zone) =>
|
case Some(inZone) =>
|
||||||
sessionActor ! SessionActor.SendResponse(
|
sessionActor ! SessionActor.SendResponse(
|
||||||
ChatMsg(
|
ChatMsg(
|
||||||
CMT_GMOPEN,
|
CMT_GMOPEN,
|
||||||
|
|
@ -313,7 +447,7 @@ class ChatActor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(zone.LivePlayers ++ zone.Corpses)
|
(inZone.LivePlayers ++ inZone.Corpses)
|
||||||
.filter(_.CharId != session.player.CharId)
|
.filter(_.CharId != session.player.CharId)
|
||||||
.sortBy(p => (p.Name, !p.isAlive))
|
.sortBy(p => (p.Name, !p.isAlive))
|
||||||
.foreach(player => {
|
.foreach(player => {
|
||||||
|
|
@ -323,7 +457,7 @@ class ChatActor(
|
||||||
CMT_GMOPEN,
|
CMT_GMOPEN,
|
||||||
message.wideContents,
|
message.wideContents,
|
||||||
"Server",
|
"Server",
|
||||||
s"${color}${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
|
s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
|
||||||
message.note
|
message.note
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -340,8 +474,8 @@ class ChatActor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case (_, _, contents) if contents.startsWith("!ntu") && gmCommandAllowed =>
|
case (_, _, content) if content.startsWith("!ntu") && gmCommandAllowed =>
|
||||||
val buffer = contents.toLowerCase.split("\\s+")
|
val buffer = content.toLowerCase.split("\\s+")
|
||||||
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
|
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
|
||||||
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
|
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
|
||||||
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
||||||
|
|
@ -363,39 +497,16 @@ class ChatActor(
|
||||||
session.zone.Buildings.values
|
session.zone.Buildings.values
|
||||||
})
|
})
|
||||||
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
|
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
|
||||||
if (silos.isEmpty) {
|
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent=s"$facility")
|
||||||
sessionActor ! SessionActor.SendResponse(
|
|
||||||
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $facility", None)
|
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
|
||||||
)
|
val buffer = contents.toLowerCase.split("\\s+")
|
||||||
}
|
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
|
||||||
customNtuValue match {
|
case Some("-list") | Some("-l") =>
|
||||||
// x = n0% of maximum capacitance
|
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
|
||||||
case Some(value) if value > -1 && value < 11 =>
|
case _ =>
|
||||||
silos.collect {
|
CavernRotationService.HurryNextRotation
|
||||||
case silo: ResourceSilo =>
|
})
|
||||||
silo.Actor ! ResourceSilo.UpdateChargeLevel(
|
|
||||||
value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// capacitance set to x (where x > 10) exactly, within limits
|
|
||||||
case Some(value) =>
|
|
||||||
silos.collect {
|
|
||||||
case silo: ResourceSilo =>
|
|
||||||
silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
// x >= n0% of maximum capacitance and x <= maximum capacitance
|
|
||||||
val rand = new scala.util.Random
|
|
||||||
silos.collect {
|
|
||||||
case silo: ResourceSilo =>
|
|
||||||
val a = 7
|
|
||||||
val b = 10 - a
|
|
||||||
val tenth = silo.MaxNtuCapacitor * 0.1f
|
|
||||||
silo.Actor ! ResourceSilo.UpdateChargeLevel(
|
|
||||||
a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
// unknown ! commands are ignored
|
// unknown ! commands are ignored
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package net.psforever.actors.session
|
||||||
|
|
||||||
import akka.actor.typed.receptionist.Receptionist
|
import akka.actor.typed.receptionist.Receptionist
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed}
|
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, OneForOneStrategy, SupervisorStrategy, typed}
|
||||||
import akka.pattern.ask
|
import akka.pattern.ask
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import net.psforever.actors.net.MiddlewareActor
|
import net.psforever.actors.net.MiddlewareActor
|
||||||
|
|
@ -17,7 +17,8 @@ import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVeh
|
||||||
import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
|
import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
|
||||||
import net.psforever.objects.equipment._
|
import net.psforever.objects.equipment._
|
||||||
import net.psforever.objects.guid._
|
import net.psforever.objects.guid._
|
||||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
||||||
|
import net.psforever.objects.loadouts.InfantryLoadout
|
||||||
import net.psforever.objects.locker.LockerContainer
|
import net.psforever.objects.locker.LockerContainer
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
import net.psforever.objects.serverobject.containable.Containable
|
import net.psforever.objects.serverobject.containable.Containable
|
||||||
|
|
@ -55,6 +56,7 @@ import net.psforever.packet._
|
||||||
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
||||||
import net.psforever.packet.game.objectcreate._
|
import net.psforever.packet.game.objectcreate._
|
||||||
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
|
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
|
||||||
|
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
||||||
import net.psforever.services.ServiceManager.{Lookup, LookupResult}
|
import net.psforever.services.ServiceManager.{Lookup, LookupResult}
|
||||||
import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
|
import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
|
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
|
||||||
|
|
@ -66,7 +68,7 @@ import net.psforever.services.properties.PropertyOverrideManager
|
||||||
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
|
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
|
||||||
import net.psforever.services.hart.HartTimer
|
import net.psforever.services.hart.HartTimer
|
||||||
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
|
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
|
||||||
import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS}
|
import net.psforever.services.{CavernRotationService, RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS}
|
||||||
import net.psforever.types._
|
import net.psforever.types._
|
||||||
import net.psforever.util.{Config, DefinitionUtil}
|
import net.psforever.util.{Config, DefinitionUtil}
|
||||||
import net.psforever.zones.Zones
|
import net.psforever.zones.Zones
|
||||||
|
|
@ -289,6 +291,118 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
var respawnTimer: Cancellable = Default.Cancellable
|
var respawnTimer: Cancellable = Default.Cancellable
|
||||||
var zoningTimer: Cancellable = Default.Cancellable
|
var zoningTimer: Cancellable = Default.Cancellable
|
||||||
|
|
||||||
|
override def supervisorStrategy: SupervisorStrategy = {
|
||||||
|
import net.psforever.objects.inventory.InventoryDisarrayException
|
||||||
|
import java.io.{StringWriter, PrintWriter}
|
||||||
|
OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
|
||||||
|
case ide: InventoryDisarrayException =>
|
||||||
|
attemptRecoverFromInventoryDisarrayException(ide.inventory)
|
||||||
|
//re-evaluate results
|
||||||
|
if (ide.inventory.ElementsOnGridMatchList() > 0) {
|
||||||
|
val sw = new StringWriter
|
||||||
|
ide.printStackTrace(new PrintWriter(sw))
|
||||||
|
log.error(sw.toString)
|
||||||
|
ImmediateDisconnect()
|
||||||
|
SupervisorStrategy.stop
|
||||||
|
} else {
|
||||||
|
SupervisorStrategy.resume
|
||||||
|
}
|
||||||
|
|
||||||
|
case e =>
|
||||||
|
val sw = new StringWriter
|
||||||
|
e.printStackTrace(new PrintWriter(sw))
|
||||||
|
log.error(sw.toString)
|
||||||
|
ImmediateDisconnect()
|
||||||
|
SupervisorStrategy.stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def attemptRecoverFromInventoryDisarrayException(inv: GridInventory): Unit = {
|
||||||
|
inv.ElementsInListCollideInGrid() match {
|
||||||
|
case Nil => ;
|
||||||
|
case overlaps =>
|
||||||
|
val previousItems = inv.Clear()
|
||||||
|
val allOverlaps = overlaps.flatten.sortBy { entry =>
|
||||||
|
val tile = entry.obj.Definition.Tile
|
||||||
|
tile.Width * tile.Height
|
||||||
|
}.toSet
|
||||||
|
val notCollidingRemainder = previousItems.filterNot(allOverlaps.contains)
|
||||||
|
notCollidingRemainder.foreach { entry =>
|
||||||
|
inv.InsertQuickly(entry.start, entry.obj)
|
||||||
|
}
|
||||||
|
var didNotFit : List[Equipment] = Nil
|
||||||
|
allOverlaps.foreach { entry =>
|
||||||
|
inv.Fit(entry.obj.Definition.Tile) match {
|
||||||
|
case Some(newStart) =>
|
||||||
|
inv.InsertQuickly(newStart, entry.obj)
|
||||||
|
case None =>
|
||||||
|
didNotFit = didNotFit :+ entry.obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//completely clear the inventory
|
||||||
|
val pguid = player.GUID
|
||||||
|
val equipmentInHand = player.Slot(player.DrawnSlot).Equipment
|
||||||
|
//redraw suit
|
||||||
|
sendResponse(ArmorChangedMessage(
|
||||||
|
pguid,
|
||||||
|
player.ExoSuit,
|
||||||
|
InfantryLoadout.DetermineSubtypeA(player.ExoSuit, equipmentInHand)
|
||||||
|
))
|
||||||
|
//redraw item in free hand (if)
|
||||||
|
player.FreeHand.Equipment match {
|
||||||
|
case Some(item) =>
|
||||||
|
sendResponse(ObjectCreateDetailedMessage(
|
||||||
|
item.Definition.ObjectId,
|
||||||
|
item.GUID,
|
||||||
|
ObjectCreateMessageParent(pguid, Player.FreeHandSlot),
|
||||||
|
item.Definition.Packet.DetailedConstructorData(item).get
|
||||||
|
))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
//redraw items in holsters
|
||||||
|
player.Holsters().zipWithIndex.foreach { case (slot, index) =>
|
||||||
|
slot.Equipment match {
|
||||||
|
case Some(item) =>
|
||||||
|
sendResponse(ObjectCreateDetailedMessage(
|
||||||
|
item.Definition.ObjectId,
|
||||||
|
item.GUID,
|
||||||
|
item.Definition.Packet.DetailedConstructorData(item).get
|
||||||
|
))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//redraw raised hand (if)
|
||||||
|
equipmentInHand match {
|
||||||
|
case Some(_) =>
|
||||||
|
sendResponse(ObjectHeldMessage(pguid, player.DrawnSlot, unk1 = true))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
//redraw inventory items
|
||||||
|
val recoveredItems = inv.Items
|
||||||
|
recoveredItems.foreach { entry =>
|
||||||
|
val item = entry.obj
|
||||||
|
sendResponse(ObjectCreateDetailedMessage(
|
||||||
|
item.Definition.ObjectId,
|
||||||
|
item.GUID,
|
||||||
|
ObjectCreateMessageParent(pguid, entry.start),
|
||||||
|
item.Definition.Packet.DetailedConstructorData(item).get
|
||||||
|
))
|
||||||
|
}
|
||||||
|
//drop items that did not fit
|
||||||
|
val placementData = PlacementData(player.Position, Vector3.z(player.Orientation.z))
|
||||||
|
didNotFit.foreach { item =>
|
||||||
|
sendResponse(ObjectCreateMessage(
|
||||||
|
item.Definition.ObjectId,
|
||||||
|
item.GUID,
|
||||||
|
DroppedItemData(
|
||||||
|
placementData,
|
||||||
|
item.Definition.Packet.ConstructorData(item).get
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def session: Session = _session
|
def session: Session = _session
|
||||||
|
|
||||||
def session_=(session: Session): Unit = {
|
def session_=(session: Session): Unit = {
|
||||||
|
|
@ -419,6 +533,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
case ICS.InterstellarClusterServiceKey.Listing(listings) =>
|
case ICS.InterstellarClusterServiceKey.Listing(listings) =>
|
||||||
cluster = listings.head
|
cluster = listings.head
|
||||||
|
|
||||||
|
case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
|
||||||
|
listings.head ! SendCavernRotationUpdates(self)
|
||||||
|
|
||||||
// Avatar subscription update
|
// Avatar subscription update
|
||||||
case avatar: Avatar =>
|
case avatar: Avatar =>
|
||||||
/*
|
/*
|
||||||
|
|
@ -601,6 +718,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
case GalaxyResponse.MapUpdate(msg) =>
|
case GalaxyResponse.MapUpdate(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
|
||||||
|
val faction = player.Faction
|
||||||
|
val from = fromFactions.contains(faction)
|
||||||
|
val to = toFactions.contains(faction)
|
||||||
|
if (from && !to) {
|
||||||
|
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
|
||||||
|
} else if (!from && to) {
|
||||||
|
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
|
||||||
|
}
|
||||||
|
|
||||||
case GalaxyResponse.FlagMapUpdate(msg) =>
|
case GalaxyResponse.FlagMapUpdate(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
|
@ -651,6 +778,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
//wait patiently
|
//wait patiently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
|
||||||
|
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
|
||||||
|
|
||||||
|
case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
|
||||||
|
sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
|
||||||
|
val popBO = 0
|
||||||
|
val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
|
||||||
|
val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
|
||||||
|
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
||||||
|
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
||||||
|
|
||||||
|
case GalaxyResponse.SendResponse(msg) =>
|
||||||
|
sendResponse(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
case LocalServiceResponse(toChannel, guid, reply) =>
|
case LocalServiceResponse(toChannel, guid, reply) =>
|
||||||
|
|
@ -1046,10 +1187,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint))
|
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint))
|
||||||
case None =>
|
case None =>
|
||||||
log.warn(
|
log.warn(
|
||||||
s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService; sending home"
|
s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService"
|
||||||
)
|
)
|
||||||
//Thread.sleep(1000) // throttle in case of infinite loop
|
if (Config.app.game.warpGates.defaultToSanctuaryDestination) {
|
||||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
log.warn(s"SpawnPointResponse: sending ${player.Name} home")
|
||||||
|
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1104,7 +1247,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
//CaptureFlagUpdateMessage()
|
//CaptureFlagUpdateMessage()
|
||||||
//VanuModuleUpdateMessage()
|
//VanuModuleUpdateMessage()
|
||||||
//ModuleLimitsMessage()
|
//ModuleLimitsMessage()
|
||||||
sendResponse(ZoneInfoMessage(continentNumber, true, 0))
|
val isCavern = continent.map.cavern
|
||||||
|
sendResponse(ZoneInfoMessage(continentNumber, true, if (isCavern) { Int.MaxValue.toLong } else { 0L }))
|
||||||
sendResponse(ZoneLockInfoMessage(continentNumber, false, true))
|
sendResponse(ZoneLockInfoMessage(continentNumber, false, true))
|
||||||
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
|
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
|
||||||
sendResponse(
|
sendResponse(
|
||||||
|
|
@ -1117,6 +1261,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
)
|
)
|
||||||
) //normally set for all zones in bulk; should be fine manually updating per zone like this
|
) //normally set for all zones in bulk; should be fine manually updating per zone like this
|
||||||
}
|
}
|
||||||
|
ServiceManager.receptionist ! Receptionist.Find(
|
||||||
|
CavernRotationService.CavernRotationServiceKey,
|
||||||
|
context.self
|
||||||
|
)
|
||||||
LivePlayerList.Add(avatar.id, avatar)
|
LivePlayerList.Add(avatar.id, avatar)
|
||||||
//PropertyOverrideMessage
|
//PropertyOverrideMessage
|
||||||
|
|
||||||
|
|
@ -1133,7 +1281,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
|
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
|
||||||
//the following subscriptions last until character switch/logout
|
//the following subscriptions last until character switch/logout
|
||||||
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
||||||
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
|
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
|
||||||
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
|
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
|
||||||
squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
||||||
player.Zone match {
|
player.Zone match {
|
||||||
|
|
@ -2085,15 +2233,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false))
|
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false))
|
||||||
//cleanup
|
//cleanup
|
||||||
(old_holsters ++ old_inventory).foreach {
|
(old_holsters ++ old_inventory).foreach {
|
||||||
case (obj, guid) =>
|
case (obj, objGuid) =>
|
||||||
sendResponse(ObjectDeleteMessage(guid, 0))
|
sendResponse(ObjectDeleteMessage(objGuid, 0))
|
||||||
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||||
}
|
}
|
||||||
//redraw
|
//redraw
|
||||||
if (maxhand) {
|
if (maxhand) {
|
||||||
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
||||||
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
||||||
0
|
slot = 0
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
|
ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
|
||||||
|
|
@ -2147,13 +2295,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||||
case Some(carrier: Player) =>
|
case Some(carrier: Player) =>
|
||||||
log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
|
log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
|
||||||
case None =>
|
case _ =>
|
||||||
log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
|
log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
|
||||||
}
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid")
|
log.warn(s"${player.toString} Tried to drop a special item that wasn't recognized. GUID: $guid")
|
||||||
}
|
}
|
||||||
|
|
||||||
case _ => ; // Nothing to drop, do nothing.
|
case _ => ; // Nothing to drop, do nothing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5653,7 +5800,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
) =>
|
) =>
|
||||||
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||||
if (deadState != DeadState.RespawnTime) {
|
if (deadState != DeadState.RespawnTime) {
|
||||||
continent.Buildings.values.find(building => building.GUID == building_guid) match {
|
continent.Buildings.values.find(_.GUID == building_guid) match {
|
||||||
case Some(wg: WarpGate) if wg.Active && (GetKnownVehicleAndSeat() match {
|
case Some(wg: WarpGate) if wg.Active && (GetKnownVehicleAndSeat() match {
|
||||||
case (Some(vehicle), _) =>
|
case (Some(vehicle), _) =>
|
||||||
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
|
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
|
||||||
|
|
@ -5665,6 +5812,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
destinationZoneGuid.guid,
|
destinationZoneGuid.guid,
|
||||||
player,
|
player,
|
||||||
destinationBuildingGuid,
|
destinationBuildingGuid,
|
||||||
|
continent.Number,
|
||||||
|
building_guid,
|
||||||
context.self
|
context.self
|
||||||
)
|
)
|
||||||
log.info(s"${player.Name} wants to use a warp gate")
|
log.info(s"${player.Name} wants to use a warp gate")
|
||||||
|
|
@ -5949,7 +6098,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
summary,
|
summary,
|
||||||
desc
|
desc
|
||||||
) =>
|
) =>
|
||||||
log.warn(s"${player.Name} filed a bug report")
|
log.warn(s"${player.Name} filed a bug report - it might be something important")
|
||||||
log.debug(s"$msg")
|
log.debug(s"$msg")
|
||||||
|
|
||||||
case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
|
||||||
|
|
@ -6843,42 +6992,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
def initGate(continentNumber: Int, buildingNumber: Int, building: Building): Unit = {
|
def initGate(continentNumber: Int, buildingNumber: Int, building: Building): Unit = {
|
||||||
building match {
|
building match {
|
||||||
case wg: WarpGate =>
|
case wg: WarpGate =>
|
||||||
sendResponse(
|
sendResponse(building.infoUpdateMessage())
|
||||||
BuildingInfoUpdateMessage(
|
|
||||||
building.Zone.Number,
|
|
||||||
building.MapId,
|
|
||||||
ntu_level = 0,
|
|
||||||
is_hacked = false,
|
|
||||||
empire_hack = PlanetSideEmpire.NEUTRAL,
|
|
||||||
hack_time_remaining = 0,
|
|
||||||
building.Faction,
|
|
||||||
unk1 = 0,
|
|
||||||
unk1x = None,
|
|
||||||
PlanetSideGeneratorState.Normal,
|
|
||||||
spawn_tubes_normal = true,
|
|
||||||
force_dome_active = false,
|
|
||||||
lattice_benefit = 0,
|
|
||||||
cavern_benefit = 0,
|
|
||||||
unk4 = Nil,
|
|
||||||
unk5 = 0,
|
|
||||||
unk6 = false,
|
|
||||||
unk7 = 8,
|
|
||||||
unk7x = None,
|
|
||||||
boost_spawn_pain = false,
|
|
||||||
boost_generator_pain = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0)))
|
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0)))
|
||||||
//TODO one faction knows which gates are broadcast for another faction?
|
if (wg.Broadcast(player.Faction)) {
|
||||||
sendResponse(
|
sendResponse(
|
||||||
BroadcastWarpgateUpdateMessage(
|
BroadcastWarpgateUpdateMessage(
|
||||||
continentNumber,
|
continentNumber,
|
||||||
buildingNumber,
|
buildingNumber,
|
||||||
wg.Broadcast(PlanetSideEmpire.TR),
|
player.Faction
|
||||||
wg.Broadcast(PlanetSideEmpire.NC),
|
)
|
||||||
wg.Broadcast(PlanetSideEmpire.VS)
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7572,6 +7696,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
|
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
|
||||||
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
|
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
|
||||||
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
|
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
|
||||||
|
ImmediateDisconnect()
|
||||||
} else {
|
} else {
|
||||||
continent.GUID(player.VehicleSeated) match {
|
continent.GUID(player.VehicleSeated) match {
|
||||||
case Some(obj: Vehicle) if !obj.Destroyed =>
|
case Some(obj: Vehicle) if !obj.Destroyed =>
|
||||||
|
|
@ -8776,8 +8901,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
//for other zones ...
|
//for other zones ...
|
||||||
//biolabs have/grant benefits
|
//biolabs have/grant benefits
|
||||||
val cryoBenefit: Float = toSpawnPoint.Owner match {
|
val cryoBenefit: Float = toSpawnPoint.Owner match {
|
||||||
case b: Building if b.hasLatticeBenefit(GlobalDefinitions.cryo_facility) => 0.5f
|
case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) => 0.5f
|
||||||
case _ => 1f
|
case _ => 1f
|
||||||
}
|
}
|
||||||
//TODO cumulative death penalty
|
//TODO cumulative death penalty
|
||||||
toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds
|
toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds
|
||||||
|
|
@ -9129,7 +9254,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
def KeepAlivePersistence(): Unit = {
|
def KeepAlivePersistence(): Unit = {
|
||||||
interimUngunnedVehicle = None
|
interimUngunnedVehicle = None
|
||||||
persist()
|
persist()
|
||||||
turnCounterFunc(player.GUID)
|
if (player.HasGUID) {
|
||||||
|
turnCounterFunc(player.GUID)
|
||||||
|
} else {
|
||||||
|
turnCounterFunc(PlanetSideGUID(0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9235,7 +9364,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
val initialQuality = tool.FireMode match {
|
val initialQuality = tool.FireMode match {
|
||||||
case mode: ChargeFireModeDefinition =>
|
case mode: ChargeFireModeDefinition =>
|
||||||
ProjectileQuality.Modified(
|
ProjectileQuality.Modified(
|
||||||
projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) / mode.Time.toFloat
|
{
|
||||||
|
val timeInterval = projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis())
|
||||||
|
timeInterval.toFloat / mode.Time.toFloat
|
||||||
|
}
|
||||||
)
|
)
|
||||||
case _ =>
|
case _ =>
|
||||||
ProjectileQuality.Normal
|
ProjectileQuality.Normal
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,44 @@
|
||||||
package net.psforever.actors.zone
|
package net.psforever.actors.zone
|
||||||
|
|
||||||
|
import akka.{actor => classic}
|
||||||
import akka.actor.typed.receptionist.Receptionist
|
import akka.actor.typed.receptionist.Receptionist
|
||||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||||
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
|
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
|
||||||
import akka.{actor => classic}
|
|
||||||
import net.psforever.actors.commands.NtuCommand
|
import net.psforever.actors.commands.NtuCommand
|
||||||
import net.psforever.objects.NtuContainer
|
import net.psforever.actors.zone.building._
|
||||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
|
||||||
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
|
|
||||||
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
|
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
|
||||||
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
|
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.persistence
|
import net.psforever.persistence
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
|
||||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||||
import net.psforever.services.{InterstellarClusterService, Service, ServiceManager}
|
import net.psforever.services.{InterstellarClusterService, ServiceManager}
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState}
|
import net.psforever.types.PlanetSideEmpire
|
||||||
import net.psforever.util.Database._
|
import net.psforever.util.Database.ctx
|
||||||
|
import org.log4s.Logger
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
final case class BuildingControlDetails(
|
||||||
|
galaxyService: classic.ActorRef = null,
|
||||||
|
interstellarCluster: ActorRef[InterstellarClusterService.Command] = null
|
||||||
|
)
|
||||||
|
|
||||||
object BuildingActor {
|
object BuildingActor {
|
||||||
def apply(zone: Zone, building: Building): Behavior[Command] =
|
def apply(zone: Zone, building: Building): Behavior[Command] =
|
||||||
Behaviors
|
Behaviors
|
||||||
.supervise[Command] {
|
.supervise[Command] {
|
||||||
Behaviors.withStash(100) { buffer =>
|
Behaviors.withStash(capacity = 100) { buffer =>
|
||||||
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building).start())
|
val logic: BuildingLogic = building match {
|
||||||
|
case _: WarpGate =>
|
||||||
|
WarpGateLogic
|
||||||
|
case _ if zone.map.cavern =>
|
||||||
|
CavernFacilityLogic
|
||||||
|
case _ if building.BuildingType == StructureType.Facility =>
|
||||||
|
MajorFacilityLogic
|
||||||
|
case _ =>
|
||||||
|
FacilityLogic
|
||||||
|
}
|
||||||
|
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building, logic).start())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure[Exception](SupervisorStrategy.restart)
|
.onFailure[Exception](SupervisorStrategy.restart)
|
||||||
|
|
@ -40,13 +51,7 @@ object BuildingActor {
|
||||||
|
|
||||||
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command
|
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command
|
||||||
|
|
||||||
final case class UpdateForceDome(state: Option[Boolean]) extends Command
|
final case class AlertToFactionChange(building: Building) extends Command
|
||||||
|
|
||||||
object UpdateForceDome {
|
|
||||||
def apply(): UpdateForceDome = UpdateForceDome(None)
|
|
||||||
|
|
||||||
def apply(state: Boolean): UpdateForceDome = UpdateForceDome(Some(state))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO remove
|
// TODO remove
|
||||||
// Changes to building objects should go through BuildingActor
|
// Changes to building objects should go through BuildingActor
|
||||||
|
|
@ -70,55 +75,93 @@ object BuildingActor {
|
||||||
final case class PowerOff() extends Command
|
final case class PowerOff() extends Command
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
|
* Set a facility affiliated to one faction to be affiliated to a different faction.
|
||||||
* The only test not employed is whether or not the target building is a capitol.
|
* @param details building and event system references
|
||||||
* Ommission of this condition makes this test capable of evaluating subcapitol eligibility
|
* @param faction faction to which the building is being set
|
||||||
* for capitol force dome expansion.
|
* @param log wrapped-up log for customized debug information
|
||||||
* @param building the target building
|
|
||||||
* @return `true`, if the conditions for capitol force dome are not met;
|
|
||||||
* `false`, otherwise
|
|
||||||
*/
|
*/
|
||||||
def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
|
def setFactionTo(
|
||||||
building.Faction == PlanetSideEmpire.NEUTRAL ||
|
details: BuildingWrapper,
|
||||||
building.NtuLevel == 0 ||
|
faction: PlanetSideEmpire.Value,
|
||||||
(building.Generator match {
|
log: BuildingWrapper => Logger
|
||||||
case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed
|
): Unit = {
|
||||||
case _ => false
|
setFactionInDatabase(details, faction, log)
|
||||||
})
|
setFactionOnEntity(details, faction, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this building is a capitol major facility,
|
* Set a facility affiliated to one faction to be affiliated to a different faction.
|
||||||
* use the faction affinity, the generator status, and the resource silo's capacitance level
|
* Handle the database entry updates to reflect the proper faction affiliation.
|
||||||
* to determine if the capitol force dome should be active.
|
* @param details building and event system references
|
||||||
* @param building the building being evaluated
|
* @param faction faction to which the building is being set
|
||||||
* @return the condition of the capitol force dome;
|
* @param log wrapped-up log for customized debug information
|
||||||
* `None`, if the facility is not a capitol building;
|
|
||||||
* `Some(true|false)` to indicate the state of the force dome
|
|
||||||
*/
|
*/
|
||||||
def checkForceDomeStatus(building: Building): Option[Boolean] = {
|
def setFactionInDatabase(
|
||||||
if (building.IsCapitol) {
|
details: BuildingWrapper,
|
||||||
val originalStatus = building.ForceDomeActive
|
faction: PlanetSideEmpire.Value,
|
||||||
val faction = building.Faction
|
log: BuildingWrapper => Logger
|
||||||
val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) {
|
): Unit = {
|
||||||
false
|
val building = details.building
|
||||||
} else {
|
val zone = building.Zone
|
||||||
val ownedSubCapitols = building.Neighbours(faction) match {
|
import ctx._
|
||||||
case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) }
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
case None => 0
|
ctx
|
||||||
}
|
.run(
|
||||||
if (originalStatus && ownedSubCapitols <= 1) {
|
query[persistence.Building]
|
||||||
false
|
.filter(_.localId == lift(building.MapId))
|
||||||
} else if (!originalStatus && ownedSubCapitols > 1) {
|
.filter(_.zoneId == lift(zone.Number))
|
||||||
true
|
)
|
||||||
} else {
|
.onComplete {
|
||||||
originalStatus
|
case Success(res) =>
|
||||||
}
|
res.headOption match {
|
||||||
|
case Some(_) =>
|
||||||
|
ctx
|
||||||
|
.run(
|
||||||
|
query[persistence.Building]
|
||||||
|
.filter(_.localId == lift(building.MapId))
|
||||||
|
.filter(_.zoneId == lift(zone.Number))
|
||||||
|
.update(_.factionId -> lift(faction.id))
|
||||||
|
)
|
||||||
|
.onComplete {
|
||||||
|
case Success(_) =>
|
||||||
|
case Failure(e) => log(details).error(e.getMessage)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
ctx
|
||||||
|
.run(
|
||||||
|
query[persistence.Building]
|
||||||
|
.insert(
|
||||||
|
_.localId -> lift(building.MapId),
|
||||||
|
_.factionId -> lift(faction.id),
|
||||||
|
_.zoneId -> lift(zone.Number)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.onComplete {
|
||||||
|
case Success(_) =>
|
||||||
|
case Failure(e) => log(details).error(e.getMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Failure(e) => log(details).error(e.getMessage)
|
||||||
}
|
}
|
||||||
Some(updatedStatus)
|
}
|
||||||
} else {
|
|
||||||
None
|
/**
|
||||||
}
|
* Set a facility affiliated to one faction to be affiliated to a different faction.
|
||||||
|
* Handle the facility entry to reflect the correct faction affiliation.
|
||||||
|
* @param details building and event system references
|
||||||
|
* @param faction faction to which the building is being set
|
||||||
|
* @param log wrapped-up log for customized debug information
|
||||||
|
*/
|
||||||
|
def setFactionOnEntity(
|
||||||
|
details: BuildingWrapper,
|
||||||
|
faction: PlanetSideEmpire.Value,
|
||||||
|
log: BuildingWrapper => Logger
|
||||||
|
): Unit = {
|
||||||
|
val building = details.building
|
||||||
|
val zone = building.Zone
|
||||||
|
building.Faction = faction
|
||||||
|
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
|
||||||
|
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,371 +169,78 @@ class BuildingActor(
|
||||||
context: ActorContext[BuildingActor.Command],
|
context: ActorContext[BuildingActor.Command],
|
||||||
buffer: StashBuffer[BuildingActor.Command],
|
buffer: StashBuffer[BuildingActor.Command],
|
||||||
zone: Zone,
|
zone: Zone,
|
||||||
building: Building
|
building: Building,
|
||||||
|
logic: BuildingLogic
|
||||||
) {
|
) {
|
||||||
|
|
||||||
import BuildingActor._
|
import BuildingActor._
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
|
||||||
var galaxyService: Option[classic.ActorRef] = None
|
|
||||||
var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
|
||||||
var hasNtuSupply: Boolean = true
|
|
||||||
|
|
||||||
context.system.receptionist ! Receptionist.Find(
|
|
||||||
InterstellarClusterService.InterstellarClusterServiceKey,
|
|
||||||
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
|
|
||||||
)
|
|
||||||
|
|
||||||
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
|
|
||||||
"galaxy",
|
|
||||||
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
|
|
||||||
)
|
|
||||||
|
|
||||||
def start(): Behavior[Command] = {
|
def start(): Behavior[Command] = {
|
||||||
|
context.system.receptionist ! Receptionist.Find(
|
||||||
|
InterstellarClusterService.InterstellarClusterServiceKey,
|
||||||
|
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
|
||||||
|
)
|
||||||
|
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
|
||||||
|
"galaxy",
|
||||||
|
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
|
||||||
|
)
|
||||||
|
setup(BuildingControlDetails())
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup(details: BuildingControlDetails): Behavior[Command] = {
|
||||||
Behaviors.receiveMessage {
|
Behaviors.receiveMessage {
|
||||||
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
|
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
|
||||||
interstellarCluster = listings.headOption
|
switchToBehavior(details.copy(interstellarCluster = listings.head))
|
||||||
postStartBehaviour()
|
|
||||||
|
|
||||||
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
|
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
|
||||||
request match {
|
switchToBehavior(request match {
|
||||||
case "galaxy" => galaxyService = Some(endpoint)
|
case "galaxy" => details.copy(galaxyService = endpoint)
|
||||||
}
|
case _ => details
|
||||||
postStartBehaviour()
|
})
|
||||||
|
|
||||||
case other =>
|
case other =>
|
||||||
buffer.stash(other)
|
buffer.stash(other)
|
||||||
Behaviors.same
|
setup(details)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def postStartBehaviour(): Behavior[Command] = {
|
def switchToBehavior(details: BuildingControlDetails): Behavior[Command] = {
|
||||||
(galaxyService, interstellarCluster) match {
|
if (details.galaxyService != null && details.interstellarCluster != null) {
|
||||||
case (Some(_galaxyService), Some(_interstellarCluster)) =>
|
buffer.unstashAll(active(logic.wrapper(building, context, details)))
|
||||||
buffer.unstashAll(active(_galaxyService, _interstellarCluster))
|
} else {
|
||||||
case _ =>
|
setup(details)
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def active(
|
def active(details: BuildingWrapper): Behavior[Command] = {
|
||||||
galaxyService: classic.ActorRef,
|
|
||||||
interstellarCluster: ActorRef[InterstellarClusterService.Command]
|
|
||||||
): Behavior[Command] = {
|
|
||||||
Behaviors.receiveMessagePartial {
|
Behaviors.receiveMessagePartial {
|
||||||
case SetFaction(faction) =>
|
case SetFaction(faction) =>
|
||||||
setFactionTo(faction, galaxyService)
|
logic.setFactionTo(details, faction)
|
||||||
|
|
||||||
|
case AlertToFactionChange(neighbor) =>
|
||||||
|
logic.alertToFactionChange(details, neighbor)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case MapUpdate() =>
|
case MapUpdate() =>
|
||||||
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
|
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case UpdateForceDome(stateOpt) =>
|
case AmenityStateChange(amenity, data) =>
|
||||||
stateOpt match {
|
logic.amenityStateChange(details, amenity, data)
|
||||||
case Some(updatedStatus) if building.IsCapitol && updatedStatus != building.ForceDomeActive =>
|
|
||||||
updateForceDomeStatus(updatedStatus, mapUpdateOnChange = true)
|
|
||||||
case _ =>
|
|
||||||
alignForceDomeStatus()
|
|
||||||
}
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case AmenityStateChange(gen: Generator, data) =>
|
|
||||||
if (generatorStateChange(gen, data)) {
|
|
||||||
// Request all buildings update their map data to refresh lattice linked benefits
|
|
||||||
zone.actor ! ZoneActor.ZoneMapUpdate()
|
|
||||||
}
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case AmenityStateChange(terminal: CaptureTerminal, data) =>
|
|
||||||
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
|
|
||||||
building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
|
|
||||||
data match {
|
|
||||||
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
|
|
||||||
case _ => log.warn("CaptureTerminal AmenityStateChange was received with no attached data.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
|
|
||||||
building.HackableAmenities.foreach(amenity => {
|
|
||||||
if (amenity.HackedBy.isDefined) {
|
|
||||||
zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// No map update needed - will be sent by `HackCaptureActor` when required
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case AmenityStateChange(_, _) =>
|
|
||||||
//TODO when parameter object is finally immutable, perform analysis on it to determine specific actions
|
|
||||||
//for now, just update the map
|
|
||||||
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case PowerOff() =>
|
case PowerOff() =>
|
||||||
building.Generator match {
|
logic.powerOff(details)
|
||||||
case Some(gen) => gen.Actor ! BuildingActor.NtuDepleted()
|
|
||||||
case _ => powerLost()
|
|
||||||
}
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case PowerOn() =>
|
case PowerOn() =>
|
||||||
building.Generator match {
|
logic.powerOn(details)
|
||||||
case Some(gen) if building.NtuLevel > 0 => gen.Actor ! BuildingActor.SuppliedWithNtu()
|
|
||||||
case _ => powerRestored()
|
|
||||||
}
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case msg @ NtuDepleted() =>
|
case NtuDepleted() =>
|
||||||
// Someone let the base run out of nanites. No one gets anything.
|
logic.ntuDepleted(details)
|
||||||
building.Amenities.foreach { amenity =>
|
|
||||||
amenity.Actor ! msg
|
|
||||||
}
|
|
||||||
setFactionTo(PlanetSideEmpire.NEUTRAL, galaxyService)
|
|
||||||
hasNtuSupply = false
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case msg @ SuppliedWithNtu() =>
|
case SuppliedWithNtu() =>
|
||||||
// Auto-repair restart, mainly. If the Generator works, power should be restored too.
|
logic.suppliedWithNtu(details)
|
||||||
hasNtuSupply = true
|
|
||||||
building.Amenities.foreach { amenity =>
|
|
||||||
amenity.Actor ! msg
|
|
||||||
}
|
|
||||||
Behaviors.same
|
|
||||||
|
|
||||||
case Ntu(msg) =>
|
case Ntu(msg) =>
|
||||||
ntu(msg)
|
logic.ntu(details, msg)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def generatorStateChange(generator: Generator, event: Any): Boolean = {
|
|
||||||
event match {
|
|
||||||
case Some(GeneratorControl.Event.UnderAttack) =>
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 15)
|
|
||||||
building.PlayersInSOI.foreach { player =>
|
|
||||||
events ! AvatarServiceMessage(player.Name, msg)
|
|
||||||
}
|
|
||||||
false
|
|
||||||
case Some(GeneratorControl.Event.Critical) =>
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val msg = AvatarAction.PlanetsideAttributeToAll(guid, 46, 1)
|
|
||||||
building.PlayersInSOI.foreach { player =>
|
|
||||||
events ! AvatarServiceMessage(player.Name, msg)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
case Some(GeneratorControl.Event.Destabilized) =>
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 16)
|
|
||||||
building.PlayersInSOI.foreach { player =>
|
|
||||||
events ! AvatarServiceMessage(player.Name, msg)
|
|
||||||
}
|
|
||||||
false
|
|
||||||
case Some(GeneratorControl.Event.Destroyed) =>
|
|
||||||
true
|
|
||||||
case Some(GeneratorControl.Event.Offline) =>
|
|
||||||
powerLost()
|
|
||||||
alignForceDomeStatus(mapUpdateOnChange = false)
|
|
||||||
val zone = building.Zone
|
|
||||||
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
|
|
||||||
building.PlayersInSOI.foreach { player =>
|
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(player.Name, msg)
|
|
||||||
} //???
|
|
||||||
true
|
|
||||||
case Some(GeneratorControl.Event.Normal) =>
|
|
||||||
true
|
|
||||||
case Some(GeneratorControl.Event.Online) =>
|
|
||||||
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
|
|
||||||
powerRestored()
|
|
||||||
alignForceDomeStatus(mapUpdateOnChange = false)
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
|
|
||||||
val msg2 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, guid, 17)
|
|
||||||
building.PlayersInSOI.foreach { player =>
|
|
||||||
val name = player.Name
|
|
||||||
events ! AvatarServiceMessage(name, msg1) //reset ???; might be global?
|
|
||||||
events ! AvatarServiceMessage(name, msg2) //This facility's generator is back on line
|
|
||||||
}
|
|
||||||
true
|
|
||||||
case _ =>
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def setFactionTo(faction: PlanetSideEmpire.Value, galaxy: classic.ActorRef): Unit = {
|
|
||||||
if (hasNtuSupply) {
|
|
||||||
import ctx._
|
|
||||||
ctx
|
|
||||||
.run(
|
|
||||||
query[persistence.Building]
|
|
||||||
.filter(_.localId == lift(building.MapId))
|
|
||||||
.filter(_.zoneId == lift(zone.Number))
|
|
||||||
)
|
|
||||||
.onComplete {
|
|
||||||
case Success(res) =>
|
|
||||||
res.headOption match {
|
|
||||||
case Some(_) =>
|
|
||||||
ctx
|
|
||||||
.run(
|
|
||||||
query[persistence.Building]
|
|
||||||
.filter(_.localId == lift(building.MapId))
|
|
||||||
.filter(_.zoneId == lift(zone.Number))
|
|
||||||
.update(_.factionId -> lift(building.Faction.id))
|
|
||||||
)
|
|
||||||
.onComplete {
|
|
||||||
case Success(_) =>
|
|
||||||
case Failure(e) => log.error(e.getMessage)
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
ctx
|
|
||||||
.run(
|
|
||||||
query[persistence.Building]
|
|
||||||
.insert(
|
|
||||||
_.localId -> lift(building.MapId),
|
|
||||||
_.factionId -> lift(building.Faction.id),
|
|
||||||
_.zoneId -> lift(zone.Number)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.onComplete {
|
|
||||||
case Success(_) =>
|
|
||||||
case Failure(e) => log.error(e.getMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case Failure(e) => log.error(e.getMessage)
|
|
||||||
}
|
|
||||||
building.Faction = faction
|
|
||||||
alignForceDomeStatus(mapUpdateOnChange = false)
|
|
||||||
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
|
|
||||||
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate the conditions of the building
|
|
||||||
* and determine if its capitol force dome state should be updated
|
|
||||||
* to reflect the actual conditions of the base or its surrounding bases.
|
|
||||||
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
|
|
||||||
* and has the capitol force dome has a dependency upon it,
|
|
||||||
* pass a message onto that facility that it should check its own state alignment.
|
|
||||||
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
|
|
||||||
*/
|
|
||||||
def alignForceDomeStatus(mapUpdateOnChange: Boolean = true): Unit = {
|
|
||||||
BuildingActor.checkForceDomeStatus(building) match {
|
|
||||||
case Some(updatedStatus) if updatedStatus != building.ForceDomeActive =>
|
|
||||||
updateForceDomeStatus(updatedStatus, mapUpdateOnChange)
|
|
||||||
case None if building.IsSubCapitol =>
|
|
||||||
building.Neighbours match {
|
|
||||||
case Some(buildings: Set[Building]) =>
|
|
||||||
buildings
|
|
||||||
.filter { _.IsCapitol }
|
|
||||||
.foreach { _.Actor ! BuildingActor.UpdateForceDome() }
|
|
||||||
case None => ;
|
|
||||||
}
|
|
||||||
case _ => ; //building is neither a capitol nor a subcapitol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
|
|
||||||
* @param updatedStatus the new capitol force dome status
|
|
||||||
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
|
|
||||||
*/
|
|
||||||
def updateForceDomeStatus(updatedStatus: Boolean, mapUpdateOnChange: Boolean): Unit = {
|
|
||||||
building.ForceDomeActive = updatedStatus
|
|
||||||
zone.LocalEvents ! LocalServiceMessage(
|
|
||||||
zone.id,
|
|
||||||
LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus)
|
|
||||||
)
|
|
||||||
if (mapUpdateOnChange) {
|
|
||||||
context.self ! BuildingActor.MapUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Power has been severed.
|
|
||||||
* All installed amenities are distributed a `PowerOff` message
|
|
||||||
* and are instructed to display their "unpowered" model.
|
|
||||||
* Additionally, the facility is now rendered unspawnable regardless of its player spawning amenities.
|
|
||||||
*/
|
|
||||||
def powerLost(): Unit = {
|
|
||||||
val zone = building.Zone
|
|
||||||
val zoneId = zone.id
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val powerMsg = BuildingActor.PowerOff()
|
|
||||||
building.Amenities.foreach { amenity =>
|
|
||||||
amenity.Actor ! powerMsg
|
|
||||||
}
|
|
||||||
//amenities disabled; red warning lights
|
|
||||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 1))
|
|
||||||
//disable spawn target on deployment map
|
|
||||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Power has been restored.
|
|
||||||
* All installed amenities are distributed a `PowerOn` message
|
|
||||||
* and are instructed to display their "powered" model.
|
|
||||||
* Additionally, the facility is now rendered spawnable if its player spawning amenities are online.
|
|
||||||
*/
|
|
||||||
def powerRestored(): Unit = {
|
|
||||||
val zone = building.Zone
|
|
||||||
val zoneId = zone.id
|
|
||||||
val events = zone.AvatarEvents
|
|
||||||
val guid = building.GUID
|
|
||||||
val powerMsg = BuildingActor.PowerOn()
|
|
||||||
building.Amenities.foreach { amenity =>
|
|
||||||
amenity.Actor ! powerMsg
|
|
||||||
}
|
|
||||||
//amenities enabled; normal lights
|
|
||||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 48, 0))
|
|
||||||
//enable spawn target on deployment map
|
|
||||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(guid, 38, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
def ntu(msg: NtuCommand.Command): Behavior[Command] = {
|
|
||||||
import NtuCommand._
|
|
||||||
msg match {
|
|
||||||
case Offer(_, _) =>
|
|
||||||
Behaviors.same
|
|
||||||
case Request(amount, replyTo) =>
|
|
||||||
building match {
|
|
||||||
case b: WarpGate =>
|
|
||||||
//warp gates are an infinite source of nanites
|
|
||||||
replyTo ! Grant(b, if (b.Active) amount else 0)
|
|
||||||
Behaviors.same
|
|
||||||
case _ if building.BuildingType == StructureType.Tower || building.Zone.map.cavern =>
|
|
||||||
//towers and cavern stuff get free repairs
|
|
||||||
replyTo ! NtuCommand.Grant(new FakeNtuSource(building), amount)
|
|
||||||
Behaviors.same
|
|
||||||
case _ =>
|
|
||||||
//all other facilities require a storage silo for ntu
|
|
||||||
building.NtuSource match {
|
|
||||||
case Some(ntuContainer) =>
|
|
||||||
ntuContainer.Actor ! msg //needs to redirect
|
|
||||||
Behaviors.same
|
|
||||||
case None =>
|
|
||||||
replyTo ! NtuCommand.Grant(null, 0)
|
|
||||||
Behaviors.unhandled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeNtuSource(private val building: Building)
|
|
||||||
extends PlanetSideServerObject
|
|
||||||
with NtuContainer {
|
|
||||||
override def NtuCapacitor = Int.MaxValue.toFloat
|
|
||||||
override def NtuCapacitor_=(a: Float) = Int.MaxValue.toFloat
|
|
||||||
override def MaxNtuCapacitor = Int.MaxValue.toFloat
|
|
||||||
override def Faction = building.Faction
|
|
||||||
override def Zone = building.Zone
|
|
||||||
override def Definition = null
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
import net.psforever.objects.ballistics.SourceEntry
|
import net.psforever.objects.ballistics.SourceEntry
|
||||||
import net.psforever.objects.ce.Deployable
|
import net.psforever.objects.ce.Deployable
|
||||||
import net.psforever.objects.equipment.Equipment
|
import net.psforever.objects.equipment.Equipment
|
||||||
import net.psforever.objects.serverobject.structures.{Building, StructureType}
|
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
|
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
|
||||||
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
|
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
|
||||||
|
|
@ -13,6 +13,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||||
|
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
|
import net.psforever.actors.zone.building.MajorFacilityLogic
|
||||||
import net.psforever.util.Database._
|
import net.psforever.util.Database._
|
||||||
import net.psforever.persistence
|
import net.psforever.persistence
|
||||||
|
|
||||||
|
|
@ -85,22 +86,19 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
||||||
|
|
||||||
ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete {
|
ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete {
|
||||||
case Success(buildings) =>
|
case Success(buildings) =>
|
||||||
var capitol: Option[Building] = None
|
|
||||||
buildings.foreach { building =>
|
buildings.foreach { building =>
|
||||||
zone.BuildingByMapId(building.localId) match {
|
zone.BuildingByMapId(building.localId) match {
|
||||||
|
case Some(_: WarpGate) => ;
|
||||||
|
//warp gates are controlled by game logic and are better off not restored via the database
|
||||||
case Some(b) =>
|
case Some(b) =>
|
||||||
b.Faction = PlanetSideEmpire(building.factionId)
|
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
|
||||||
if(b.IsCapitol) {
|
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
|
||||||
capitol = Some(b)
|
b.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(b) }
|
||||||
}
|
}
|
||||||
case None =>
|
case None => ;
|
||||||
// TODO this happens during testing, need a way to not always persist during tests
|
// TODO this happens during testing, need a way to not always persist during tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
capitol match {
|
|
||||||
case Some(b) => b.ForceDomeActive = BuildingActor.checkForceDomeStatus(b).getOrElse(false)
|
|
||||||
case None => ;
|
|
||||||
}
|
|
||||||
case Failure(e) => log.error(e.getMessage)
|
case Failure(e) => log.error(e.getMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +119,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
||||||
case PickupItem(guid) =>
|
case PickupItem(guid) =>
|
||||||
zone.Ground ! Zone.Ground.PickupItem(guid)
|
zone.Ground ! Zone.Ground.PickupItem(guid)
|
||||||
|
|
||||||
case BuildDeployable(obj, tool) =>
|
case BuildDeployable(obj, _) =>
|
||||||
zone.Deployables ! Zone.Deployable.Build(obj)
|
zone.Deployables ! Zone.Deployable.Build(obj)
|
||||||
|
|
||||||
case DismissDeployable(obj) =>
|
case DismissDeployable(obj) =>
|
||||||
|
|
|
||||||
|
|
@ -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.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3}
|
||||||
import net.psforever.types._
|
import net.psforever.types._
|
||||||
import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition}
|
import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition}
|
||||||
|
import net.psforever.objects.serverobject.terminals.tabs._
|
||||||
import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier
|
import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier
|
||||||
|
|
||||||
import scala.annotation.switch
|
import scala.annotation.switch
|
||||||
|
|
@ -1261,11 +1262,33 @@ object GlobalDefinitions {
|
||||||
/*
|
/*
|
||||||
Buildings
|
Buildings
|
||||||
*/
|
*/
|
||||||
val building = new BuildingDefinition(474) { Name = "building" } //borrows object id of entity mainbase1
|
val amp_station = new BuildingDefinition(45) {
|
||||||
val amp_station = new BuildingDefinition(45) { Name = "amp_station"; SOIRadius = 300 }
|
Name = "amp_station"
|
||||||
val comm_station = new BuildingDefinition(211) { Name = "comm_station"; SOIRadius = 300 }
|
SOIRadius = 300
|
||||||
val comm_station_dsp = new BuildingDefinition(212) { Name = "comm_station_dsp"; SOIRadius = 300 }
|
LatticeLinkBenefit = LatticeBenefit.AmpStation
|
||||||
val cryo_facility = new BuildingDefinition(215) { Name = "cryo_facility"; SOIRadius = 300 }
|
}
|
||||||
|
val comm_station = new BuildingDefinition(211) {
|
||||||
|
Name = "comm_station"
|
||||||
|
SOIRadius = 300
|
||||||
|
LatticeLinkBenefit = LatticeBenefit.InterlinkFacility
|
||||||
|
}
|
||||||
|
val comm_station_dsp = new BuildingDefinition(212) {
|
||||||
|
Name = "comm_station_dsp"
|
||||||
|
SOIRadius = 300
|
||||||
|
LatticeLinkBenefit = LatticeBenefit.DropshipCenter
|
||||||
|
}
|
||||||
|
val cryo_facility = new BuildingDefinition(215) {
|
||||||
|
Name = "cryo_facility"
|
||||||
|
SOIRadius = 300
|
||||||
|
LatticeLinkBenefit = LatticeBenefit.BioLaboratory
|
||||||
|
}
|
||||||
|
val tech_plant = new BuildingDefinition(852) {
|
||||||
|
Name = "tech_plant"
|
||||||
|
SOIRadius = 300
|
||||||
|
LatticeLinkBenefit = LatticeBenefit.TechnologyPlant
|
||||||
|
}
|
||||||
|
|
||||||
|
val building = new BuildingDefinition(474) { Name = "building" } //borrows object id of entity mainbase1
|
||||||
|
|
||||||
val vanu_core = new BuildingDefinition(932) { Name = "vanu_core" }
|
val vanu_core = new BuildingDefinition(932) { Name = "vanu_core" }
|
||||||
|
|
||||||
|
|
@ -1293,6 +1316,22 @@ object GlobalDefinitions {
|
||||||
val ceiling_bldg_j = new BuildingDefinition(474) { Name = "ceiling_bldg_j" } //borrows object id of entity mainbase1
|
val ceiling_bldg_j = new BuildingDefinition(474) { Name = "ceiling_bldg_j" } //borrows object id of entity mainbase1
|
||||||
val ceiling_bldg_z = new BuildingDefinition(474) { Name = "ceiling_bldg_z" } //borrows object id of entity mainbase1
|
val ceiling_bldg_z = new BuildingDefinition(474) { Name = "ceiling_bldg_z" } //borrows object id of entity mainbase1
|
||||||
|
|
||||||
|
val mainbase1 = new BuildingDefinition(474) { Name = "mainbase1" }
|
||||||
|
val mainbase2 = new BuildingDefinition(475) { Name = "mainbase2" }
|
||||||
|
val mainbase3 = new BuildingDefinition(476) { Name = "mainbase3" }
|
||||||
|
val meeting_center_nc = new BuildingDefinition(537) { Name = "meeting_center_nc" }
|
||||||
|
val meeting_center_tr = new BuildingDefinition(538) { Name = "meeting_center_tr" }
|
||||||
|
val meeting_center_vs = new BuildingDefinition(539) { Name = "meeting_center_vs" }
|
||||||
|
val minibase1 = new BuildingDefinition(557) { Name = "minibase1" }
|
||||||
|
val minibase2 = new BuildingDefinition(558) { Name = "minibase2" }
|
||||||
|
val minibase3 = new BuildingDefinition(559) { Name = "minibase3" }
|
||||||
|
val redoubt = new BuildingDefinition(726) { Name = "redoubt"; SOIRadius = 187 }
|
||||||
|
val tower_a = new BuildingDefinition(869) { Name = "tower_a"; SOIRadius = 50 }
|
||||||
|
val tower_b = new BuildingDefinition(870) { Name = "tower_b"; SOIRadius = 50 }
|
||||||
|
val tower_c = new BuildingDefinition(871) { Name = "tower_c"; SOIRadius = 50 }
|
||||||
|
val vanu_control_point = new BuildingDefinition(931) { Name = "vanu_control_point"; SOIRadius = 187 }
|
||||||
|
val vanu_vehicle_station = new BuildingDefinition(948) { Name = "vanu_vehicle_station"; SOIRadius = 187 }
|
||||||
|
|
||||||
val hst = new WarpGateDefinition(402)
|
val hst = new WarpGateDefinition(402)
|
||||||
hst.Name = "hst"
|
hst.Name = "hst"
|
||||||
hst.UseRadius = 20.4810f
|
hst.UseRadius = 20.4810f
|
||||||
|
|
@ -1309,23 +1348,6 @@ object GlobalDefinitions {
|
||||||
hst.NoWarp += peregrine_flight
|
hst.NoWarp += peregrine_flight
|
||||||
hst.SpecificPointFunc = SpawnPoint.Gate
|
hst.SpecificPointFunc = SpawnPoint.Gate
|
||||||
|
|
||||||
val mainbase1 = new BuildingDefinition(474) { Name = "mainbase1" }
|
|
||||||
val mainbase2 = new BuildingDefinition(475) { Name = "mainbase2" }
|
|
||||||
val mainbase3 = new BuildingDefinition(476) { Name = "mainbase3" }
|
|
||||||
val meeting_center_nc = new BuildingDefinition(537) { Name = "meeting_center_nc" }
|
|
||||||
val meeting_center_tr = new BuildingDefinition(538) { Name = "meeting_center_tr" }
|
|
||||||
val meeting_center_vs = new BuildingDefinition(539) { Name = "meeting_center_vs" }
|
|
||||||
val minibase1 = new BuildingDefinition(557) { Name = "minibase1" }
|
|
||||||
val minibase2 = new BuildingDefinition(558) { Name = "minibase2" }
|
|
||||||
val minibase3 = new BuildingDefinition(559) { Name = "minibase3" }
|
|
||||||
val redoubt = new BuildingDefinition(726) { Name = "redoubt"; SOIRadius = 187 }
|
|
||||||
val tech_plant = new BuildingDefinition(852) { Name = "tech_plant"; SOIRadius = 300 }
|
|
||||||
val tower_a = new BuildingDefinition(869) { Name = "tower_a"; SOIRadius = 50 }
|
|
||||||
val tower_b = new BuildingDefinition(870) { Name = "tower_b"; SOIRadius = 50 }
|
|
||||||
val tower_c = new BuildingDefinition(871) { Name = "tower_c"; SOIRadius = 50 }
|
|
||||||
val vanu_control_point = new BuildingDefinition(931) { Name = "vanu_control_point"; SOIRadius = 187 }
|
|
||||||
val vanu_vehicle_station = new BuildingDefinition(948) { Name = "vanu_vehicle_station"; SOIRadius = 187 }
|
|
||||||
|
|
||||||
val warpgate = new WarpGateDefinition(993)
|
val warpgate = new WarpGateDefinition(993)
|
||||||
warpgate.Name = "warpgate"
|
warpgate.Name = "warpgate"
|
||||||
warpgate.UseRadius = 301.8713f
|
warpgate.UseRadius = 301.8713f
|
||||||
|
|
@ -1338,7 +1360,7 @@ object GlobalDefinitions {
|
||||||
warpgate_cavern.UseRadius = 51.0522f
|
warpgate_cavern.UseRadius = 51.0522f
|
||||||
warpgate_cavern.SOIRadius = 52
|
warpgate_cavern.SOIRadius = 52
|
||||||
warpgate_cavern.VehicleAllowance = true
|
warpgate_cavern.VehicleAllowance = true
|
||||||
warpgate_cavern.SpecificPointFunc = SpawnPoint.Gate
|
warpgate_cavern.SpecificPointFunc = SpawnPoint.HalfHighGate
|
||||||
|
|
||||||
val warpgate_small = new WarpGateDefinition(995)
|
val warpgate_small = new WarpGateDefinition(995)
|
||||||
warpgate_small.Name = "warpgate_small"
|
warpgate_small.Name = "warpgate_small"
|
||||||
|
|
@ -3466,7 +3488,7 @@ object GlobalDefinitions {
|
||||||
|
|
||||||
maelstrom_grenade_damager.Name = "maelstrom_grenade_damager"
|
maelstrom_grenade_damager.Name = "maelstrom_grenade_damager"
|
||||||
maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct
|
maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct
|
||||||
//todo the maelstrom_grenade_damage is something of a broken entity atm
|
//the maelstrom_grenade_damage is something of a broken entity atm
|
||||||
|
|
||||||
maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile"
|
maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile"
|
||||||
maelstrom_grenade_projectile.Damage0 = 32
|
maelstrom_grenade_projectile.Damage0 = 32
|
||||||
|
|
@ -3953,7 +3975,7 @@ object GlobalDefinitions {
|
||||||
ProjectileDefinition.CalculateDerivedFields(quasar_projectile)
|
ProjectileDefinition.CalculateDerivedFields(quasar_projectile)
|
||||||
|
|
||||||
radiator_cloud.Name = "radiator_cloud"
|
radiator_cloud.Name = "radiator_cloud"
|
||||||
radiator_cloud.Damage0 = 2
|
radiator_cloud.Damage0 = 1 //2
|
||||||
radiator_cloud.DamageAtEdge = 1.0f
|
radiator_cloud.DamageAtEdge = 1.0f
|
||||||
radiator_cloud.DamageRadius = 5f
|
radiator_cloud.DamageRadius = 5f
|
||||||
radiator_cloud.DamageToHealthOnly = true
|
radiator_cloud.DamageToHealthOnly = true
|
||||||
|
|
@ -5106,7 +5128,6 @@ object GlobalDefinitions {
|
||||||
spiker.FireModes.head.AmmoSlotIndex = 0
|
spiker.FireModes.head.AmmoSlotIndex = 0
|
||||||
spiker.FireModes.head.Magazine = 25
|
spiker.FireModes.head.Magazine = 25
|
||||||
spiker.Tile = InventoryTile.Tile33
|
spiker.Tile = InventoryTile.Tile33
|
||||||
//TODO the spiker is weird
|
|
||||||
|
|
||||||
mini_chaingun.Name = "mini_chaingun"
|
mini_chaingun.Name = "mini_chaingun"
|
||||||
mini_chaingun.Size = EquipmentSize.Rifle
|
mini_chaingun.Size = EquipmentSize.Rifle
|
||||||
|
|
@ -5182,7 +5203,6 @@ object GlobalDefinitions {
|
||||||
maelstrom.FireModes(2).Magazine = 150
|
maelstrom.FireModes(2).Magazine = 150
|
||||||
maelstrom.FireModes(2).RoundsPerShot = 10
|
maelstrom.FireModes(2).RoundsPerShot = 10
|
||||||
maelstrom.Tile = InventoryTile.Tile93
|
maelstrom.Tile = InventoryTile.Tile93
|
||||||
//TODO the maelstrom is weird
|
|
||||||
|
|
||||||
phoenix.Name = "phoenix"
|
phoenix.Name = "phoenix"
|
||||||
phoenix.Size = EquipmentSize.Rifle
|
phoenix.Size = EquipmentSize.Rifle
|
||||||
|
|
@ -8835,7 +8855,7 @@ object GlobalDefinitions {
|
||||||
colossus_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
colossus_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
||||||
colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f)
|
colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f)
|
||||||
colossus_flight.MaxCapacitor = 156
|
colossus_flight.MaxCapacitor = 156
|
||||||
colossus_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor
|
colossus_flight.DefaultCapacitor = colossus_flight.MaxCapacitor
|
||||||
colossus_flight.CapacitorDrain = 16
|
colossus_flight.CapacitorDrain = 16
|
||||||
colossus_flight.CapacitorDrainSpecial = 3
|
colossus_flight.CapacitorDrainSpecial = 3
|
||||||
colossus_flight.CapacitorRecharge = 42
|
colossus_flight.CapacitorRecharge = 42
|
||||||
|
|
@ -8889,7 +8909,7 @@ object GlobalDefinitions {
|
||||||
peregrine_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
peregrine_flight.UnderwaterLifespan(suffocation = 60000L, recovery = 30000L)
|
||||||
peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f)
|
peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f)
|
||||||
peregrine_flight.MaxCapacitor = 156
|
peregrine_flight.MaxCapacitor = 156
|
||||||
peregrine_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor
|
peregrine_flight.DefaultCapacitor = peregrine_flight.MaxCapacitor
|
||||||
peregrine_flight.CapacitorDrain = 16
|
peregrine_flight.CapacitorDrain = 16
|
||||||
peregrine_flight.CapacitorDrainSpecial = 3
|
peregrine_flight.CapacitorDrainSpecial = 3
|
||||||
peregrine_flight.CapacitorRecharge = 42
|
peregrine_flight.CapacitorRecharge = 42
|
||||||
|
|
@ -9303,21 +9323,29 @@ object GlobalDefinitions {
|
||||||
spawn_terminal.Name = "spawn_terminal"
|
spawn_terminal.Name = "spawn_terminal"
|
||||||
spawn_terminal.Damageable = false
|
spawn_terminal.Damageable = false
|
||||||
spawn_terminal.Repairable = false
|
spawn_terminal.Repairable = false
|
||||||
spawn_terminal.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default value?
|
spawn_terminal.autoRepair = AutoRepairStats(1, 5000, 200, 1)
|
||||||
|
|
||||||
order_terminal.Name = "order_terminal"
|
order_terminal.Name = "order_terminal"
|
||||||
order_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminal.Tab += 0 -> {
|
||||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
val tab = EquipmentPage(
|
||||||
)
|
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||||
order_terminal.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
order_terminal.Tab += 1 -> ArmorWithAmmoPage(
|
||||||
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
||||||
EquipmentTerminalDefinition.maxAmmo
|
EquipmentTerminalDefinition.maxAmmo
|
||||||
)
|
)
|
||||||
order_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminal.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||||
)
|
)
|
||||||
order_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
order_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||||
order_terminal.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
order_terminal.Tab += 4 -> {
|
||||||
|
val tab = InfantryLoadoutPage()
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
order_terminal.SellEquipmentByDefault = true
|
order_terminal.SellEquipmentByDefault = true
|
||||||
order_terminal.MaxHealth = 500
|
order_terminal.MaxHealth = 500
|
||||||
order_terminal.Damageable = true
|
order_terminal.Damageable = true
|
||||||
|
|
@ -9328,61 +9356,75 @@ object GlobalDefinitions {
|
||||||
order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f)
|
order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f)
|
||||||
|
|
||||||
order_terminala.Name = "order_terminala"
|
order_terminala.Name = "order_terminala"
|
||||||
order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminala.Tab += 0 -> {
|
||||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
val tab = EquipmentPage(
|
||||||
)
|
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||||
order_terminala.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
)
|
||||||
|
tab.Exclude = List(NoCavernEquipmentRule)
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
order_terminala.Tab += 1 -> ArmorWithAmmoPage(
|
||||||
EquipmentTerminalDefinition.suits,
|
EquipmentTerminalDefinition.suits,
|
||||||
EquipmentTerminalDefinition.maxAmmo
|
EquipmentTerminalDefinition.maxAmmo
|
||||||
)
|
)
|
||||||
order_terminala.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminala.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||||
)
|
)
|
||||||
order_terminala.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
order_terminala.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||||
order_terminala.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
order_terminala.Tab += 4 -> {
|
||||||
order_terminala.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX
|
val tab = InfantryLoadoutPage()
|
||||||
|
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
|
||||||
|
tab
|
||||||
|
}
|
||||||
order_terminala.SellEquipmentByDefault = true
|
order_terminala.SellEquipmentByDefault = true
|
||||||
order_terminala.Damageable = false
|
order_terminala.Damageable = false
|
||||||
order_terminala.Repairable = false
|
order_terminala.Repairable = false
|
||||||
|
|
||||||
order_terminalb.Name = "order_terminalb"
|
order_terminalb.Name = "order_terminalb"
|
||||||
order_terminalb.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminalb.Tab += 0 -> {
|
||||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
val tab = EquipmentPage(
|
||||||
)
|
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||||
order_terminalb.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
)
|
||||||
|
tab.Exclude = List(NoCavernEquipmentRule)
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
order_terminalb.Tab += 1 -> ArmorWithAmmoPage(
|
||||||
EquipmentTerminalDefinition.suits,
|
EquipmentTerminalDefinition.suits,
|
||||||
EquipmentTerminalDefinition.maxAmmo
|
EquipmentTerminalDefinition.maxAmmo
|
||||||
)
|
)
|
||||||
order_terminalb.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
order_terminalb.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||||
)
|
)
|
||||||
order_terminalb.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
order_terminalb.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||||
order_terminalb.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
order_terminalb.Tab += 4 -> {
|
||||||
order_terminalb.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX
|
val tab = InfantryLoadoutPage()
|
||||||
|
tab.Exclude = List(NoExoSuitRule(ExoSuitType.MAX), NoCavernEquipmentRule)
|
||||||
|
tab
|
||||||
|
}
|
||||||
order_terminalb.SellEquipmentByDefault = true
|
order_terminalb.SellEquipmentByDefault = true
|
||||||
order_terminalb.Damageable = false
|
order_terminalb.Damageable = false
|
||||||
order_terminalb.Repairable = false
|
order_terminalb.Repairable = false
|
||||||
|
|
||||||
vanu_equipment_term.Name = "vanu_equipment_term"
|
vanu_equipment_term.Name = "vanu_equipment_term"
|
||||||
vanu_equipment_term.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(
|
vanu_equipment_term.Tab += 0 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons
|
||||||
)
|
)
|
||||||
vanu_equipment_term.Tab += 1 -> OrderTerminalDefinition.ArmorWithAmmoPage(
|
vanu_equipment_term.Tab += 1 -> ArmorWithAmmoPage(
|
||||||
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
EquipmentTerminalDefinition.suits ++ EquipmentTerminalDefinition.maxSuits,
|
||||||
EquipmentTerminalDefinition.maxAmmo
|
EquipmentTerminalDefinition.maxAmmo
|
||||||
)
|
)
|
||||||
vanu_equipment_term.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
vanu_equipment_term.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons
|
||||||
)
|
)
|
||||||
vanu_equipment_term.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
vanu_equipment_term.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||||
vanu_equipment_term.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage()
|
vanu_equipment_term.Tab += 4 -> InfantryLoadoutPage()
|
||||||
vanu_equipment_term.SellEquipmentByDefault = true
|
vanu_equipment_term.SellEquipmentByDefault = true
|
||||||
vanu_equipment_term.Damageable = false
|
vanu_equipment_term.Damageable = false
|
||||||
vanu_equipment_term.Repairable = false
|
vanu_equipment_term.Repairable = false
|
||||||
|
|
||||||
cert_terminal.Name = "cert_terminal"
|
cert_terminal.Name = "cert_terminal"
|
||||||
val certs = Certification.values.filter(_.cost != 0)
|
val certs = Certification.values.filter(_.cost != 0)
|
||||||
val page = OrderTerminalDefinition.CertificationPage(certs)
|
val page = CertificationPage(certs)
|
||||||
cert_terminal.Tab += 0 -> page
|
cert_terminal.Tab += 0 -> page
|
||||||
cert_terminal.MaxHealth = 500
|
cert_terminal.MaxHealth = 500
|
||||||
cert_terminal.Damageable = true
|
cert_terminal.Damageable = true
|
||||||
|
|
@ -9402,20 +9444,28 @@ object GlobalDefinitions {
|
||||||
implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f)
|
implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f)
|
||||||
|
|
||||||
implant_terminal_interface.Name = "implant_terminal_interface"
|
implant_terminal_interface.Name = "implant_terminal_interface"
|
||||||
implant_terminal_interface.Tab += 0 -> OrderTerminalDefinition.ImplantPage(ImplantTerminalDefinition.implants)
|
implant_terminal_interface.Tab += 0 -> ImplantPage(ImplantTerminalDefinition.implants)
|
||||||
implant_terminal_interface.MaxHealth = 500
|
implant_terminal_interface.MaxHealth = 500
|
||||||
implant_terminal_interface.Damageable = false //TODO true
|
implant_terminal_interface.Damageable = false //TODO true
|
||||||
implant_terminal_interface.Repairable = true
|
implant_terminal_interface.Repairable = true
|
||||||
implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default value?
|
implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1)
|
||||||
implant_terminal_interface.RepairIfDestroyed = true
|
implant_terminal_interface.RepairIfDestroyed = true
|
||||||
//TODO will need geometry when Damageable = true
|
//TODO will need geometry when Damageable = true
|
||||||
|
|
||||||
ground_vehicle_terminal.Name = "ground_vehicle_terminal"
|
ground_vehicle_terminal.Name = "ground_vehicle_terminal"
|
||||||
ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
ground_vehicle_terminal.Tab += 46769 -> {
|
||||||
VehicleTerminalDefinition.groundVehicles,
|
val tab = VehiclePage(
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.groundVehicles,
|
||||||
)
|
VehicleTerminalDefinition.trunk
|
||||||
ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
)
|
||||||
|
tab.Exclude = List(CavernVehicleQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
ground_vehicle_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
ground_vehicle_terminal.MaxHealth = 500
|
ground_vehicle_terminal.MaxHealth = 500
|
||||||
ground_vehicle_terminal.Damageable = true
|
ground_vehicle_terminal.Damageable = true
|
||||||
ground_vehicle_terminal.Repairable = true
|
ground_vehicle_terminal.Repairable = true
|
||||||
|
|
@ -9425,11 +9475,15 @@ object GlobalDefinitions {
|
||||||
ground_vehicle_terminal.Geometry = vterm
|
ground_vehicle_terminal.Geometry = vterm
|
||||||
|
|
||||||
air_vehicle_terminal.Name = "air_vehicle_terminal"
|
air_vehicle_terminal.Name = "air_vehicle_terminal"
|
||||||
air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
air_vehicle_terminal.Tab += 46769 -> VehiclePage(
|
||||||
VehicleTerminalDefinition.flight1Vehicles,
|
VehicleTerminalDefinition.flight1Vehicles,
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.trunk
|
||||||
)
|
)
|
||||||
air_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
air_vehicle_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernVehicleQuestion, CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
air_vehicle_terminal.MaxHealth = 500
|
air_vehicle_terminal.MaxHealth = 500
|
||||||
air_vehicle_terminal.Damageable = true
|
air_vehicle_terminal.Damageable = true
|
||||||
air_vehicle_terminal.Repairable = true
|
air_vehicle_terminal.Repairable = true
|
||||||
|
|
@ -9439,11 +9493,15 @@ object GlobalDefinitions {
|
||||||
air_vehicle_terminal.Geometry = vterm
|
air_vehicle_terminal.Geometry = vterm
|
||||||
|
|
||||||
dropship_vehicle_terminal.Name = "dropship_vehicle_terminal"
|
dropship_vehicle_terminal.Name = "dropship_vehicle_terminal"
|
||||||
dropship_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
dropship_vehicle_terminal.Tab += 46769 -> VehiclePage(
|
||||||
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles,
|
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles,
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.trunk
|
||||||
)
|
)
|
||||||
dropship_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
dropship_vehicle_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
dropship_vehicle_terminal.MaxHealth = 500
|
dropship_vehicle_terminal.MaxHealth = 500
|
||||||
dropship_vehicle_terminal.Damageable = true
|
dropship_vehicle_terminal.Damageable = true
|
||||||
dropship_vehicle_terminal.Repairable = true
|
dropship_vehicle_terminal.Repairable = true
|
||||||
|
|
@ -9453,11 +9511,19 @@ object GlobalDefinitions {
|
||||||
dropship_vehicle_terminal.Geometry = vterm
|
dropship_vehicle_terminal.Geometry = vterm
|
||||||
|
|
||||||
vehicle_terminal_combined.Name = "vehicle_terminal_combined"
|
vehicle_terminal_combined.Name = "vehicle_terminal_combined"
|
||||||
vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
vehicle_terminal_combined.Tab += 46769 -> {
|
||||||
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles,
|
val tab = VehiclePage(
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles,
|
||||||
)
|
VehicleTerminalDefinition.trunk
|
||||||
vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
)
|
||||||
|
tab.Exclude = List(CavernVehicleQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
vehicle_terminal_combined.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
vehicle_terminal_combined.MaxHealth = 500
|
vehicle_terminal_combined.MaxHealth = 500
|
||||||
vehicle_terminal_combined.Damageable = true
|
vehicle_terminal_combined.Damageable = true
|
||||||
vehicle_terminal_combined.Repairable = true
|
vehicle_terminal_combined.Repairable = true
|
||||||
|
|
@ -9467,11 +9533,11 @@ object GlobalDefinitions {
|
||||||
vehicle_terminal_combined.Geometry = vterm
|
vehicle_terminal_combined.Geometry = vterm
|
||||||
|
|
||||||
vanu_air_vehicle_term.Name = "vanu_air_vehicle_term"
|
vanu_air_vehicle_term.Name = "vanu_air_vehicle_term"
|
||||||
vanu_air_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
vanu_air_vehicle_term.Tab += 46769 -> VehiclePage(
|
||||||
VehicleTerminalDefinition.flight1Vehicles,
|
VehicleTerminalDefinition.flight1Vehicles,
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.trunk
|
||||||
)
|
)
|
||||||
vanu_air_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
vanu_air_vehicle_term.Tab += 4 -> VehicleLoadoutPage(10)
|
||||||
vanu_air_vehicle_term.MaxHealth = 500
|
vanu_air_vehicle_term.MaxHealth = 500
|
||||||
vanu_air_vehicle_term.Damageable = true
|
vanu_air_vehicle_term.Damageable = true
|
||||||
vanu_air_vehicle_term.Repairable = true
|
vanu_air_vehicle_term.Repairable = true
|
||||||
|
|
@ -9480,11 +9546,11 @@ object GlobalDefinitions {
|
||||||
vanu_air_vehicle_term.Subtract.Damage1 = 8
|
vanu_air_vehicle_term.Subtract.Damage1 = 8
|
||||||
|
|
||||||
vanu_vehicle_term.Name = "vanu_vehicle_term"
|
vanu_vehicle_term.Name = "vanu_vehicle_term"
|
||||||
vanu_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(
|
vanu_vehicle_term.Tab += 46769 -> VehiclePage(
|
||||||
VehicleTerminalDefinition.groundVehicles,
|
VehicleTerminalDefinition.groundVehicles,
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.trunk
|
||||||
)
|
)
|
||||||
vanu_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
vanu_vehicle_term.Tab += 4 -> VehicleLoadoutPage(10)
|
||||||
vanu_vehicle_term.MaxHealth = 500
|
vanu_vehicle_term.MaxHealth = 500
|
||||||
vanu_vehicle_term.Damageable = true
|
vanu_vehicle_term.Damageable = true
|
||||||
vanu_vehicle_term.Repairable = true
|
vanu_vehicle_term.Repairable = true
|
||||||
|
|
@ -9493,19 +9559,21 @@ object GlobalDefinitions {
|
||||||
vanu_vehicle_term.Subtract.Damage1 = 8
|
vanu_vehicle_term.Subtract.Damage1 = 8
|
||||||
|
|
||||||
bfr_terminal.Name = "bfr_terminal"
|
bfr_terminal.Name = "bfr_terminal"
|
||||||
bfr_terminal.Tab += 0 -> OrderTerminalDefinition.VehiclePage(
|
bfr_terminal.Tab += 0 -> VehiclePage(
|
||||||
VehicleTerminalDefinition.bfrVehicles,
|
VehicleTerminalDefinition.bfrVehicles,
|
||||||
VehicleTerminalDefinition.trunk
|
VehicleTerminalDefinition.trunk
|
||||||
)
|
)
|
||||||
bfr_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage(
|
bfr_terminal.Tab += 1 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
||||||
) //inaccessible?
|
) //inaccessible?
|
||||||
bfr_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
bfr_terminal.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
|
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
|
||||||
) //inaccessible?
|
) //inaccessible?
|
||||||
bfr_terminal.Tab += 3 -> OrderTerminalDefinition.BattleframeSpawnLoadoutPage(
|
bfr_terminal.Tab += 3 -> {
|
||||||
VehicleTerminalDefinition.bfrVehicles
|
val tab = BattleframeSpawnLoadoutPage(VehicleTerminalDefinition.bfrVehicles)
|
||||||
)
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
bfr_terminal.MaxHealth = 500
|
bfr_terminal.MaxHealth = 500
|
||||||
bfr_terminal.Damageable = true
|
bfr_terminal.Damageable = true
|
||||||
bfr_terminal.Repairable = true
|
bfr_terminal.Repairable = true
|
||||||
|
|
@ -9521,7 +9589,7 @@ object GlobalDefinitions {
|
||||||
respawn_tube.Damageable = true
|
respawn_tube.Damageable = true
|
||||||
respawn_tube.DamageableByFriendlyFire = false
|
respawn_tube.DamageableByFriendlyFire = false
|
||||||
respawn_tube.Repairable = true
|
respawn_tube.Repairable = true
|
||||||
respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value?
|
respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
|
||||||
respawn_tube.RepairIfDestroyed = true
|
respawn_tube.RepairIfDestroyed = true
|
||||||
respawn_tube.Subtract.Damage1 = 8
|
respawn_tube.Subtract.Damage1 = 8
|
||||||
respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
|
respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
|
||||||
|
|
@ -9533,7 +9601,7 @@ object GlobalDefinitions {
|
||||||
respawn_tube_sanctuary.Damageable = false //true?
|
respawn_tube_sanctuary.Damageable = false //true?
|
||||||
respawn_tube_sanctuary.DamageableByFriendlyFire = false
|
respawn_tube_sanctuary.DamageableByFriendlyFire = false
|
||||||
respawn_tube_sanctuary.Repairable = true
|
respawn_tube_sanctuary.Repairable = true
|
||||||
respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value?
|
respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
|
||||||
//TODO will need geometry when Damageable = true
|
//TODO will need geometry when Damageable = true
|
||||||
|
|
||||||
respawn_tube_tower.Name = "respawn_tube_tower"
|
respawn_tube_tower.Name = "respawn_tube_tower"
|
||||||
|
|
@ -9543,18 +9611,18 @@ object GlobalDefinitions {
|
||||||
respawn_tube_tower.Damageable = true
|
respawn_tube_tower.Damageable = true
|
||||||
respawn_tube_tower.DamageableByFriendlyFire = false
|
respawn_tube_tower.DamageableByFriendlyFire = false
|
||||||
respawn_tube_tower.Repairable = true
|
respawn_tube_tower.Repairable = true
|
||||||
respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //TODO drain is default value?
|
respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1)
|
||||||
respawn_tube_tower.RepairIfDestroyed = true
|
respawn_tube_tower.RepairIfDestroyed = true
|
||||||
respawn_tube_tower.Subtract.Damage1 = 8
|
respawn_tube_tower.Subtract.Damage1 = 8
|
||||||
respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
|
respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f)
|
||||||
|
|
||||||
teleportpad_terminal.Name = "teleportpad_terminal"
|
teleportpad_terminal.Name = "teleportpad_terminal"
|
||||||
teleportpad_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.routerTerminal)
|
teleportpad_terminal.Tab += 0 -> EquipmentPage(EquipmentTerminalDefinition.routerTerminal)
|
||||||
teleportpad_terminal.Damageable = false
|
teleportpad_terminal.Damageable = false
|
||||||
teleportpad_terminal.Repairable = false
|
teleportpad_terminal.Repairable = false
|
||||||
|
|
||||||
targeting_laser_dispenser.Name = "targeting_laser_dispenser"
|
targeting_laser_dispenser.Name = "targeting_laser_dispenser"
|
||||||
targeting_laser_dispenser.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.flailTerminal)
|
targeting_laser_dispenser.Tab += 0 -> EquipmentPage(EquipmentTerminalDefinition.flailTerminal)
|
||||||
targeting_laser_dispenser.Damageable = false
|
targeting_laser_dispenser.Damageable = false
|
||||||
targeting_laser_dispenser.Repairable = false
|
targeting_laser_dispenser.Repairable = false
|
||||||
|
|
||||||
|
|
@ -9791,38 +9859,54 @@ object GlobalDefinitions {
|
||||||
lodestar_repair_terminal.Repairable = false
|
lodestar_repair_terminal.Repairable = false
|
||||||
|
|
||||||
multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal"
|
multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal"
|
||||||
multivehicle_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(
|
multivehicle_rearm_terminal.Tab += 3 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.vehicleAmmunition
|
EquipmentTerminalDefinition.vehicleAmmunition
|
||||||
)
|
)
|
||||||
multivehicle_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
multivehicle_rearm_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
||||||
multivehicle_rearm_terminal.Damageable = false
|
multivehicle_rearm_terminal.Damageable = false
|
||||||
multivehicle_rearm_terminal.Repairable = false
|
multivehicle_rearm_terminal.Repairable = false
|
||||||
|
|
||||||
bfr_rearm_terminal.Name = "bfr_rearm_terminal"
|
bfr_rearm_terminal.Name = "bfr_rearm_terminal"
|
||||||
bfr_rearm_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage(
|
bfr_rearm_terminal.Tab += 1 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons
|
||||||
)
|
)
|
||||||
bfr_rearm_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(
|
bfr_rearm_terminal.Tab += 2 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
|
EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons
|
||||||
)
|
)
|
||||||
bfr_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.VehicleLoadoutPage(15)
|
bfr_rearm_terminal.Tab += 3 -> {
|
||||||
|
val tab = VehicleLoadoutPage(15)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
||||||
bfr_rearm_terminal.Damageable = false
|
bfr_rearm_terminal.Damageable = false
|
||||||
bfr_rearm_terminal.Repairable = false
|
bfr_rearm_terminal.Repairable = false
|
||||||
|
|
||||||
air_rearm_terminal.Name = "air_rearm_terminal"
|
air_rearm_terminal.Name = "air_rearm_terminal"
|
||||||
air_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
air_rearm_terminal.Tab += 3 -> EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition)
|
||||||
air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
air_rearm_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
air_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
air_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
||||||
air_rearm_terminal.Damageable = false
|
air_rearm_terminal.Damageable = false
|
||||||
air_rearm_terminal.Repairable = false
|
air_rearm_terminal.Repairable = false
|
||||||
|
|
||||||
ground_rearm_terminal.Name = "ground_rearm_terminal"
|
ground_rearm_terminal.Name = "ground_rearm_terminal"
|
||||||
ground_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(
|
ground_rearm_terminal.Tab += 3 -> EquipmentPage(
|
||||||
EquipmentTerminalDefinition.vehicleAmmunition
|
EquipmentTerminalDefinition.vehicleAmmunition
|
||||||
)
|
)
|
||||||
ground_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10)
|
ground_rearm_terminal.Tab += 4 -> {
|
||||||
|
val tab = VehicleLoadoutPage(10)
|
||||||
|
tab.Exclude = List(CavernEquipmentQuestion)
|
||||||
|
tab
|
||||||
|
}
|
||||||
ground_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
ground_rearm_terminal.SellEquipmentByDefault = true //TODO ?
|
||||||
ground_rearm_terminal.Damageable = false
|
ground_rearm_terminal.Damageable = false
|
||||||
ground_rearm_terminal.Repairable = false
|
ground_rearm_terminal.Repairable = false
|
||||||
|
|
@ -9942,7 +10026,7 @@ object GlobalDefinitions {
|
||||||
generator.Damageable = true
|
generator.Damageable = true
|
||||||
generator.DamageableByFriendlyFire = false
|
generator.DamageableByFriendlyFire = false
|
||||||
generator.Repairable = true
|
generator.Repairable = true
|
||||||
generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) //TODO drain is default value?
|
generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1)
|
||||||
generator.RepairDistance = 13.5f
|
generator.RepairDistance = 13.5f
|
||||||
generator.RepairIfDestroyed = true
|
generator.RepairIfDestroyed = true
|
||||||
generator.Subtract.Damage1 = 9
|
generator.Subtract.Damage1 = 9
|
||||||
|
|
|
||||||
|
|
@ -212,8 +212,12 @@ class Player(var avatar: Avatar)
|
||||||
def HolsterItems(): List[InventoryItem] = holsters
|
def HolsterItems(): List[InventoryItem] = holsters
|
||||||
.zipWithIndex
|
.zipWithIndex
|
||||||
.collect {
|
.collect {
|
||||||
case (slot: EquipmentSlot, index: Int) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index)
|
case (slot: EquipmentSlot, index: Int) =>
|
||||||
}.toList
|
slot.Equipment match {
|
||||||
|
case Some(item) => Some(InventoryItem(item, index))
|
||||||
|
case None => None
|
||||||
|
}
|
||||||
|
}.flatten.toList
|
||||||
|
|
||||||
def Inventory: GridInventory = inventory
|
def Inventory: GridInventory = inventory
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,16 @@ object SpawnPoint {
|
||||||
Default(obj, target)
|
Default(obj, target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def HalfHighGate(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = {
|
||||||
|
val (a, b) = Gate(obj, target)
|
||||||
|
target match {
|
||||||
|
case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) =>
|
||||||
|
(a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b)
|
||||||
|
case _ =>
|
||||||
|
(a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait SpawnPointDefinition {
|
trait SpawnPointDefinition {
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
|
||||||
zone.GUID(obj.Telepad) match {
|
zone.GUID(obj.Telepad) match {
|
||||||
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
|
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
|
||||||
oldTpad.Actor ! TelepadLike.SeverLink(obj)
|
oldTpad.Actor ! TelepadLike.SeverLink(obj)
|
||||||
case None => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
obj.Telepad = tpad.GUID
|
obj.Telepad = tpad.GUID
|
||||||
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))
|
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ class GridInventory extends Container {
|
||||||
Success(collisions.toList)
|
Success(collisions.toList)
|
||||||
} catch {
|
} catch {
|
||||||
case e: NoSuchElementException =>
|
case e: NoSuchElementException =>
|
||||||
Failure(InventoryDisarrayException(s"inventory contained old item data", e))
|
Failure(InventoryDisarrayException(s"inventory contained old item data", e, this))
|
||||||
case e: Exception =>
|
case e: Exception =>
|
||||||
Failure(e)
|
Failure(e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ package net.psforever.objects.inventory
|
||||||
* @param message the explanation of why the exception was thrown
|
* @param message the explanation of why the exception was thrown
|
||||||
* @param cause any prior `Exception` that was thrown then wrapped in this one
|
* @param cause any prior `Exception` that was thrown then wrapped in this one
|
||||||
*/
|
*/
|
||||||
final case class InventoryDisarrayException(private val message: String = "", private val cause: Throwable)
|
final case class InventoryDisarrayException(
|
||||||
|
private val message: String,
|
||||||
|
private val cause: Throwable,
|
||||||
|
inventory: GridInventory
|
||||||
|
)
|
||||||
extends Exception(message, cause)
|
extends Exception(message, cause)
|
||||||
|
|
||||||
object InventoryDisarrayException {
|
object InventoryDisarrayException {
|
||||||
|
|
@ -21,6 +25,6 @@ object InventoryDisarrayException {
|
||||||
* @param message the explanation of why the exception was thrown
|
* @param message the explanation of why the exception was thrown
|
||||||
* @return an `InventoryDisarrayException` object
|
* @return an `InventoryDisarrayException` object
|
||||||
*/
|
*/
|
||||||
def apply(message: String): InventoryDisarrayException =
|
def apply(message: String, inventory: GridInventory): InventoryDisarrayException =
|
||||||
InventoryDisarrayException(message, None.orNull)
|
InventoryDisarrayException(message, None.orNull, inventory)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -652,10 +652,11 @@ object ContainableBehavior {
|
||||||
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
|
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
|
||||||
* Used to filter through lists of object data before it is placed into a player's inventory.
|
* Used to filter through lists of object data before it is placed into a player's inventory.
|
||||||
* Drop the item if:<br>
|
* Drop the item if:<br>
|
||||||
* - the item is cavern equipment<br>
|
|
||||||
* - the item is a `BoomerTrigger` type object<br>
|
* - the item is a `BoomerTrigger` type object<br>
|
||||||
* - the item is a `router_telepad` type object<br>
|
* - the item is a `router_telepad` type object<br>
|
||||||
* - the item is another faction's exclusive equipment
|
* - the item is another faction's exclusive equipment<br>
|
||||||
|
* Additional equipment filtration information can be found attached to terminals.
|
||||||
|
* @see `ExclusionRule`
|
||||||
* @param tplayer the player
|
* @param tplayer the player
|
||||||
* @return true if the item is to be dropped; false, otherwise
|
* @return true if the item is to be dropped; false, otherwise
|
||||||
*/
|
*/
|
||||||
|
|
@ -663,7 +664,6 @@ object ContainableBehavior {
|
||||||
entry => {
|
entry => {
|
||||||
val objDef = entry.obj.Definition
|
val objDef = entry.obj.Definition
|
||||||
val faction = GlobalDefinitions.isFactionEquipment(objDef)
|
val faction = GlobalDefinitions.isFactionEquipment(objDef)
|
||||||
GlobalDefinitions.isCavernEquipment(objDef) ||
|
|
||||||
objDef == GlobalDefinitions.router_telepad ||
|
objDef == GlobalDefinitions.router_telepad ||
|
||||||
entry.obj.isInstanceOf[BoomerTrigger] ||
|
entry.obj.isInstanceOf[BoomerTrigger] ||
|
||||||
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)
|
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import java.util.concurrent.TimeUnit
|
||||||
import akka.actor.ActorContext
|
import akka.actor.ActorContext
|
||||||
import net.psforever.actors.zone.BuildingActor
|
import net.psforever.actors.zone.BuildingActor
|
||||||
import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player}
|
import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player}
|
||||||
import net.psforever.objects.definition.ObjectDefinition
|
|
||||||
import net.psforever.objects.serverobject.generator.Generator
|
import net.psforever.objects.serverobject.generator.Generator
|
||||||
import net.psforever.objects.serverobject.hackable.Hackable
|
import net.psforever.objects.serverobject.hackable.Hackable
|
||||||
import net.psforever.objects.serverobject.painbox.Painbox
|
import net.psforever.objects.serverobject.painbox.Painbox
|
||||||
|
|
@ -15,7 +14,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
|
import net.psforever.types._
|
||||||
import scalax.collection.{Graph, GraphEdge}
|
import scalax.collection.{Graph, GraphEdge}
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
||||||
|
|
@ -88,10 +87,19 @@ class Building(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all lattice neighbours
|
// Get all lattice neighbours
|
||||||
def Neighbours: Option[Set[Building]] = {
|
def AllNeighbours: Option[Set[Building]] = {
|
||||||
zone.Lattice find this match {
|
zone.Lattice find this match {
|
||||||
case Some(x) => Some(x.diSuccessors.map(x => x.toOuter))
|
case Some(x) => Some(x.diSuccessors.map(x => x.toOuter))
|
||||||
case None => None;
|
case None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all lattice neighbours that are active
|
||||||
|
// This is important because warp gates can be inactive
|
||||||
|
def Neighbours: Option[Set[Building]] = {
|
||||||
|
AllNeighbours collect {
|
||||||
|
case wg: WarpGate if wg.Active => wg
|
||||||
|
case b => b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,6 +197,15 @@ class Building(
|
||||||
val o = Amenities.collect({ case tube: SpawnTube if !tube.Destroyed => tube })
|
val o = Amenities.collect({ case tube: SpawnTube if !tube.Destroyed => tube })
|
||||||
(o.nonEmpty, false) //TODO poll pain field strength
|
(o.nonEmpty, false) //TODO poll pain field strength
|
||||||
}
|
}
|
||||||
|
val cavernBenefit: Set[CavernBenefit] = if (
|
||||||
|
generatorState != PlanetSideGeneratorState.Destroyed &&
|
||||||
|
faction != PlanetSideEmpire.NEUTRAL &&
|
||||||
|
connectedCavern().nonEmpty
|
||||||
|
) {
|
||||||
|
Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule)
|
||||||
|
} else {
|
||||||
|
Set(CavernBenefit.None)
|
||||||
|
}
|
||||||
|
|
||||||
BuildingInfoUpdateMessage(
|
BuildingInfoUpdateMessage(
|
||||||
Zone.Number,
|
Zone.Number,
|
||||||
|
|
@ -198,25 +215,29 @@ class Building(
|
||||||
hackingFaction,
|
hackingFaction,
|
||||||
hackTime,
|
hackTime,
|
||||||
if (ntuLevel > 0) Faction else PlanetSideEmpire.NEUTRAL,
|
if (ntuLevel > 0) Faction else PlanetSideEmpire.NEUTRAL,
|
||||||
0, // unk1 Field != 0 will cause malformed packet
|
unk1 = 0, // unk1 != 0 will cause malformed packet
|
||||||
None, // unk1x
|
unk1x = None,
|
||||||
generatorState,
|
generatorState,
|
||||||
spawnTubesNormal,
|
spawnTubesNormal,
|
||||||
forceDomeActive,
|
forceDomeActive,
|
||||||
if (generatorState != PlanetSideGeneratorState.Destroyed) latticeBenefitsValue() else 0,
|
latticeConnectedFacilityBenefits(),
|
||||||
if (generatorState != PlanetSideGeneratorState.Destroyed) 48 else 0, // cavern benefit
|
cavernBenefit,
|
||||||
Nil, // unk4,
|
unk4 = Nil,
|
||||||
0, // unk5
|
unk5 = 0,
|
||||||
false, // unk6
|
unk6 = false,
|
||||||
8, // unk7 Field != 8 will cause malformed packet
|
unk7 = 8, // unk7 != 8 will cause malformed packet
|
||||||
None, // unk7x
|
unk7x = None,
|
||||||
boostSpawnPain,
|
boostSpawnPain,
|
||||||
boostGeneratorPain
|
boostGeneratorPain
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = {
|
def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
|
||||||
if (Faction == PlanetSideEmpire.NEUTRAL) {
|
val genState = Generator match {
|
||||||
|
case Some(obj) => obj.Condition != PlanetSideGeneratorState.Destroyed
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
if (genState || Faction == PlanetSideEmpire.NEUTRAL) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
// Check this Building is on the lattice first
|
// Check this Building is on the lattice first
|
||||||
|
|
@ -237,16 +258,16 @@ class Building(
|
||||||
}
|
}
|
||||||
|
|
||||||
private def findLatticeBenefit(
|
private def findLatticeBenefit(
|
||||||
wantedBenefit: ObjectDefinition,
|
wantedBenefit: LatticeBenefit,
|
||||||
subGraph: Graph[Building, GraphEdge.UnDiEdge]
|
subGraph: Graph[Building, GraphEdge.UnDiEdge]
|
||||||
): Boolean = {
|
): Boolean = {
|
||||||
var found = false
|
var found = false
|
||||||
subGraph find this match {
|
subGraph find this match {
|
||||||
case Some(self) =>
|
case Some(self) =>
|
||||||
if (this.Definition == wantedBenefit) {
|
if (this.Definition.LatticeLinkBenefit == wantedBenefit) {
|
||||||
found = true
|
found = true
|
||||||
} else {
|
} else {
|
||||||
self pathUntil (_.Definition == wantedBenefit) match {
|
self pathUntil (_.Definition.LatticeLinkBenefit == wantedBenefit) match {
|
||||||
case Some(_) => found = true
|
case Some(_) => found = true
|
||||||
case None => ;
|
case None => ;
|
||||||
}
|
}
|
||||||
|
|
@ -256,54 +277,60 @@ class Building(
|
||||||
found
|
found
|
||||||
}
|
}
|
||||||
|
|
||||||
def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = {
|
def latticeConnectedFacilityBenefits(): Set[LatticeBenefit] = {
|
||||||
if (Faction == PlanetSideEmpire.NEUTRAL) {
|
val genState = Generator match {
|
||||||
Set.empty
|
case Some(obj) => obj.Condition
|
||||||
|
case _ => PlanetSideGeneratorState.Normal
|
||||||
|
}
|
||||||
|
if (genState == PlanetSideGeneratorState.Destroyed || Faction == PlanetSideEmpire.NEUTRAL) {
|
||||||
|
Set(LatticeBenefit.None)
|
||||||
} else {
|
} else {
|
||||||
// Check this Building is on the lattice first
|
friendlyFunctionalNeighborhood().map { _.Definition.LatticeLinkBenefit }
|
||||||
zone.Lattice find this match {
|
|
||||||
case Some(_) =>
|
|
||||||
val subGraph = Zone.Lattice filter ((b: Building) =>
|
|
||||||
b.Faction == this.Faction
|
|
||||||
&& !b.CaptureTerminalIsHacked
|
|
||||||
&& b.NtuLevel > 0
|
|
||||||
&& (b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed)
|
|
||||||
)
|
|
||||||
|
|
||||||
import scala.collection.mutable
|
|
||||||
var connectedBases: mutable.Set[ObjectDefinition] = mutable.Set()
|
|
||||||
if (findLatticeBenefit(GlobalDefinitions.amp_station, subGraph)) {
|
|
||||||
connectedBases.add(GlobalDefinitions.amp_station)
|
|
||||||
}
|
|
||||||
if (findLatticeBenefit(GlobalDefinitions.comm_station_dsp, subGraph)) {
|
|
||||||
connectedBases.add(GlobalDefinitions.comm_station_dsp)
|
|
||||||
}
|
|
||||||
if (findLatticeBenefit(GlobalDefinitions.cryo_facility, subGraph)) {
|
|
||||||
connectedBases.add(GlobalDefinitions.cryo_facility)
|
|
||||||
}
|
|
||||||
if (findLatticeBenefit(GlobalDefinitions.comm_station, subGraph)) {
|
|
||||||
connectedBases.add(GlobalDefinitions.comm_station)
|
|
||||||
}
|
|
||||||
if (findLatticeBenefit(GlobalDefinitions.tech_plant, subGraph)) {
|
|
||||||
connectedBases.add(GlobalDefinitions.tech_plant)
|
|
||||||
}
|
|
||||||
connectedBases.toSet
|
|
||||||
case None =>
|
|
||||||
Set.empty
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def latticeBenefitsValue(): Int = {
|
def friendlyFunctionalNeighborhood(): Set[Building] = {
|
||||||
latticeConnectedFacilityBenefits().collect {
|
var (currBuilding, newNeighbors) = Neighbours(faction).getOrElse(Set.empty[Building]).toList.splitAt(1)
|
||||||
case GlobalDefinitions.amp_station => 1
|
var visitedNeighbors: Set[Int] = Set(MapId)
|
||||||
case GlobalDefinitions.comm_station_dsp => 2
|
var friendlyNeighborhood: List[Building] = List(this)
|
||||||
case GlobalDefinitions.cryo_facility => 4
|
while (currBuilding.nonEmpty) {
|
||||||
case GlobalDefinitions.comm_station => 8
|
val building = currBuilding.head
|
||||||
case GlobalDefinitions.tech_plant => 16
|
val neighborsToAdd = if (!visitedNeighbors.contains(building.MapId)
|
||||||
}.sum
|
&& (building match { case _ : WarpGate => false; case _ => true })
|
||||||
|
&& !building.CaptureTerminalIsHacked
|
||||||
|
&& building.NtuLevel > 0
|
||||||
|
&& (building.Generator match {
|
||||||
|
case Some(o) => o.Condition != PlanetSideGeneratorState.Destroyed
|
||||||
|
case _ => true
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
visitedNeighbors = visitedNeighbors ++ Set(building.MapId)
|
||||||
|
friendlyNeighborhood = friendlyNeighborhood :+ building
|
||||||
|
building.Neighbours(faction)
|
||||||
|
.getOrElse(Set.empty[Building])
|
||||||
|
.toList
|
||||||
|
.filterNot { b => visitedNeighbors.contains(b.MapId) }
|
||||||
|
} else {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
val allocatedNeighbors = newNeighbors ++ neighborsToAdd
|
||||||
|
currBuilding = allocatedNeighbors.take(1)
|
||||||
|
newNeighbors = allocatedNeighbors.drop(1)
|
||||||
|
}
|
||||||
|
friendlyNeighborhood.toSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting from an overworld zone facility,
|
||||||
|
* find a lattice connected cavern facility that is the same faction as this starting building.
|
||||||
|
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
|
||||||
|
* do not let the search escape the current zone into another.
|
||||||
|
* If we start in a cavern zone, do not continue a fruitless search;
|
||||||
|
* just fail.
|
||||||
|
* @return the discovered faction-aligned cavern facility
|
||||||
|
*/
|
||||||
|
def connectedCavern(): Option[Building] = net.psforever.objects.zones.Zone.findConnectedCavernFacility(building = this)
|
||||||
|
|
||||||
def BuildingType: StructureType = buildingType
|
def BuildingType: StructureType = buildingType
|
||||||
|
|
||||||
override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor
|
override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,30 @@ package net.psforever.objects.serverobject.structures
|
||||||
|
|
||||||
import net.psforever.objects.{NtuContainerDefinition, SpawnPointDefinition}
|
import net.psforever.objects.{NtuContainerDefinition, SpawnPointDefinition}
|
||||||
import net.psforever.objects.definition.ObjectDefinition
|
import net.psforever.objects.definition.ObjectDefinition
|
||||||
|
import net.psforever.types.{CaptureBenefit, CavernBenefit, LatticeBenefit}
|
||||||
|
|
||||||
class BuildingDefinition(objectId: Int) extends ObjectDefinition(objectId) with NtuContainerDefinition with SphereOfInfluence {
|
class BuildingDefinition(objectId: Int)
|
||||||
|
extends ObjectDefinition(objectId)
|
||||||
|
with NtuContainerDefinition
|
||||||
|
with SphereOfInfluence {
|
||||||
Name = "building"
|
Name = "building"
|
||||||
MaxNtuCapacitor = Int.MaxValue
|
MaxNtuCapacitor = Int.MaxValue
|
||||||
|
private var latBenefit: LatticeBenefit = LatticeBenefit.None
|
||||||
|
private var cavBenefit: CavernBenefit = CavernBenefit.None
|
||||||
|
|
||||||
|
def LatticeLinkBenefit: LatticeBenefit = latBenefit
|
||||||
|
|
||||||
|
def LatticeLinkBenefit_=(bfit: LatticeBenefit): CaptureBenefit = {
|
||||||
|
latBenefit = bfit
|
||||||
|
LatticeLinkBenefit
|
||||||
|
}
|
||||||
|
|
||||||
|
def CavernLinkBenefit: CavernBenefit = cavBenefit
|
||||||
|
|
||||||
|
def CavernLinkBenefit_=(cfit: CavernBenefit): CavernBenefit = {
|
||||||
|
cavBenefit = cfit
|
||||||
|
CavernLinkBenefit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WarpGateDefinition(objectId: Int) extends BuildingDefinition(objectId) with SpawnPointDefinition
|
class WarpGateDefinition(objectId: Int) extends BuildingDefinition(objectId) with SpawnPointDefinition
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||||
import net.psforever.objects.{GlobalDefinitions, NtuContainer, SpawnPoint}
|
import net.psforever.objects.{GlobalDefinitions, NtuContainer, SpawnPoint}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
|
import net.psforever.types._
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import net.psforever.actors.zone.BuildingActor
|
import net.psforever.actors.zone.BuildingActor
|
||||||
import net.psforever.objects.definition.ObjectDefinition
|
|
||||||
|
|
||||||
import scala.collection.mutable
|
|
||||||
|
|
||||||
class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildingDefinition: WarpGateDefinition)
|
class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildingDefinition: WarpGateDefinition)
|
||||||
extends Building(name, building_guid, map_id, zone, StructureType.WarpGate, buildingDefinition)
|
extends Building(name, building_guid, map_id, zone, StructureType.WarpGate, buildingDefinition)
|
||||||
|
|
@ -21,31 +18,31 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
|
||||||
private var active: Boolean = true
|
private var active: Boolean = true
|
||||||
|
|
||||||
/** what faction views this warp gate as a broadcast gate */
|
/** what faction views this warp gate as a broadcast gate */
|
||||||
private var broadcast: mutable.Set[PlanetSideEmpire.Value] = mutable.Set.empty[PlanetSideEmpire.Value]
|
private var passageFor: Set[PlanetSideEmpire.Value] = Set(PlanetSideEmpire.NEUTRAL)
|
||||||
|
|
||||||
override def infoUpdateMessage(): BuildingInfoUpdateMessage = {
|
override def infoUpdateMessage(): BuildingInfoUpdateMessage = {
|
||||||
BuildingInfoUpdateMessage(
|
BuildingInfoUpdateMessage(
|
||||||
Zone.Number,
|
Zone.Number,
|
||||||
MapId,
|
MapId,
|
||||||
0,
|
0,
|
||||||
false,
|
is_hacked = false,
|
||||||
PlanetSideEmpire.NEUTRAL,
|
PlanetSideEmpire.NEUTRAL,
|
||||||
0L,
|
0L,
|
||||||
Faction,
|
Faction, //should be neutral in most cases
|
||||||
0, //!! Field != 0 will cause malformed packet. See class def.
|
0, //Field != 0 will cause malformed packet. See class def.
|
||||||
None,
|
None,
|
||||||
PlanetSideGeneratorState.Normal,
|
PlanetSideGeneratorState.Normal,
|
||||||
true, //TODO?
|
spawn_tubes_normal = true,
|
||||||
false, //force_dome_active
|
force_dome_active = false,
|
||||||
0, //lattice_benefit
|
Set(LatticeBenefit.None),
|
||||||
0, //cavern_benefit; !! Field > 0 will cause malformed packet. See class def.
|
Set(CavernBenefit.None), //Field > 0 will cause malformed packet. See class def.
|
||||||
Nil,
|
Nil,
|
||||||
0,
|
0,
|
||||||
false,
|
unk6 = false,
|
||||||
8, //!! Field != 8 will cause malformed packet. See class def.
|
8, //!! Field != 8 will cause malformed packet. See class def.
|
||||||
None,
|
None,
|
||||||
false, //boost_spawn_pain
|
boost_spawn_pain = false,
|
||||||
false //boost_generator_pain
|
boost_generator_pain = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,54 +69,34 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
|
||||||
/**
|
/**
|
||||||
* Determine whether any faction interacts with this warp gate as "broadcast."
|
* Determine whether any faction interacts with this warp gate as "broadcast."
|
||||||
* The gate must be active first.
|
* The gate must be active first.
|
||||||
|
* A broadcast gate allows specific factions only.
|
||||||
* @return `true`, if some faction sees this warp gate as a "broadcast gate";
|
* @return `true`, if some faction sees this warp gate as a "broadcast gate";
|
||||||
* `false`, otherwise
|
* `false`, otherwise
|
||||||
*/
|
*/
|
||||||
def Broadcast: Boolean = Active && broadcast.nonEmpty
|
def Broadcast: Boolean = Active && !passageFor.contains(PlanetSideEmpire.NEUTRAL)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a specific faction interacts with this warp gate as "broadcast."
|
* Determine whether a specific faction interacts with this warp gate as "broadcast."
|
||||||
* The warp gate being `NEUTRAL` should allow for any polled faction to interact.
|
|
||||||
* The gate must be active first.
|
|
||||||
* @return `true`, if the given faction interacts with this warp gate as a "broadcast gate";
|
* @return `true`, if the given faction interacts with this warp gate as a "broadcast gate";
|
||||||
* `false`, otherwise
|
* `false`, otherwise
|
||||||
*/
|
*/
|
||||||
def Broadcast(faction: PlanetSideEmpire.Value): Boolean = {
|
def Broadcast(faction: PlanetSideEmpire.Value): Boolean = {
|
||||||
Active && (broadcast.contains(faction) || broadcast.contains(PlanetSideEmpire.NEUTRAL))
|
Broadcast && passageFor.contains(faction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle whether the warp gate's faction-affiliated force interacts with this warp gate as "broadcast."
|
* Which factions interact with this warp gate as "broadcast"?
|
||||||
* Other "broadcast" associations are not affected.
|
|
||||||
* The gate must be active first.
|
|
||||||
* @param bcast `true`, if the faction-affiliated force interacts with this gate as broadcast;
|
|
||||||
* `false`, if not
|
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||||
*/
|
*/
|
||||||
def Broadcast_=(bcast: Boolean): Set[PlanetSideEmpire.Value] = {
|
def AllowBroadcastFor: Set[PlanetSideEmpire.Value] = passageFor
|
||||||
if (Active) {
|
|
||||||
if (bcast) {
|
|
||||||
broadcast += Faction
|
|
||||||
} else {
|
|
||||||
broadcast -= Faction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
broadcast.toSet
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Which factions interact with this warp gate as "broadcast?"
|
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
|
||||||
*/
|
|
||||||
def BroadcastFor: Set[PlanetSideEmpire.Value] = broadcast.toSet
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow a faction to interact with a given warp gate as "broadcast" if it is active.
|
* Allow a faction to interact with a given warp gate as "broadcast" if it is active.
|
||||||
* @param bcast the faction
|
* @param bcast the faction
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||||
*/
|
*/
|
||||||
def BroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
|
def AllowBroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
|
||||||
(broadcast += bcast).toSet
|
AllowBroadcastFor_=(Set(bcast))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -127,26 +104,14 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
|
||||||
* @param bcast the factions
|
* @param bcast the factions
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
* @return the set of all factions who interact with this warp gate as "broadcast"
|
||||||
*/
|
*/
|
||||||
def BroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
|
def AllowBroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
|
||||||
(broadcast ++= bcast).toSet
|
val validFactions = bcast.filterNot(_ == PlanetSideEmpire.NEUTRAL)
|
||||||
}
|
passageFor = if (bcast.isEmpty || validFactions.isEmpty) {
|
||||||
|
Set(PlanetSideEmpire.NEUTRAL)
|
||||||
/**
|
} else {
|
||||||
* Disallow a faction to interact with a given warp gate as "broadcast."
|
validFactions
|
||||||
* @param bcast the faction
|
}
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
AllowBroadcastFor
|
||||||
*/
|
|
||||||
def StopBroadcastFor_=(bcast: PlanetSideEmpire.Value): Set[PlanetSideEmpire.Value] = {
|
|
||||||
(broadcast -= bcast).toSet
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disallow some factions to interact with a given warp gate as "broadcast."
|
|
||||||
* @param bcast the factions
|
|
||||||
* @return the set of all factions who interact with this warp gate as "broadcast"
|
|
||||||
*/
|
|
||||||
def StopBroadcastFor_=(bcast: Set[PlanetSideEmpire.Value]): Set[PlanetSideEmpire.Value] = {
|
|
||||||
(broadcast --= bcast).toSet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def Owner: PlanetSideServerObject = this
|
def Owner: PlanetSideServerObject = this
|
||||||
|
|
@ -157,11 +122,13 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
|
||||||
|
|
||||||
def MaxNtuCapacitor : Float = Int.MaxValue
|
def MaxNtuCapacitor : Float = Int.MaxValue
|
||||||
|
|
||||||
|
override def isOffline: Boolean = !Active
|
||||||
|
|
||||||
override def NtuSource: Option[NtuContainer] = Some(this)
|
override def NtuSource: Option[NtuContainer] = Some(this)
|
||||||
|
|
||||||
override def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = false
|
override def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = false
|
||||||
|
|
||||||
override def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = Set.empty
|
override def latticeConnectedFacilityBenefits(): Set[LatticeBenefit] = Set.empty[LatticeBenefit]
|
||||||
|
|
||||||
override def Definition: WarpGateDefinition = buildingDefinition
|
override def Definition: WarpGateDefinition = buildingDefinition
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,12 @@
|
||||||
package net.psforever.objects.serverobject.terminals
|
package net.psforever.objects.serverobject.terminals
|
||||||
|
|
||||||
import akka.actor.{ActorContext, ActorRef}
|
import akka.actor.{ActorContext, ActorRef}
|
||||||
import net.psforever.objects.avatar.Certification
|
import net.psforever.objects.{Default, Player}
|
||||||
import net.psforever.objects.definition.ImplantDefinition
|
|
||||||
import net.psforever.objects.{Default, Player, Vehicle}
|
|
||||||
import net.psforever.objects.equipment.Equipment
|
|
||||||
import net.psforever.objects.loadouts.{InfantryLoadout, VehicleLoadout}
|
|
||||||
import net.psforever.objects.inventory.InventoryItem
|
|
||||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||||
import net.psforever.objects.serverobject.structures.Amenity
|
import net.psforever.objects.serverobject.structures.Amenity
|
||||||
import net.psforever.packet.game.ItemTransactionMessage
|
import net.psforever.packet.game.ItemTransactionMessage
|
||||||
import net.psforever.objects.serverobject.terminals.EquipmentTerminalDefinition._
|
import net.psforever.objects.serverobject.terminals.tabs.Tab
|
||||||
import net.psforever.types.{ExoSuitType, TransactionType}
|
import net.psforever.types.TransactionType
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
||||||
|
|
@ -37,8 +32,8 @@ import scala.collection.mutable
|
||||||
class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
||||||
|
|
||||||
/** An internal object organizing the different specification options found on a terminal's UI. */
|
/** An internal object organizing the different specification options found on a terminal's UI. */
|
||||||
private val tabs: mutable.HashMap[Int, OrderTerminalDefinition.Tab] =
|
private val tabs: mutable.HashMap[Int, Tab] =
|
||||||
new mutable.HashMap[Int, OrderTerminalDefinition.Tab]()
|
new mutable.HashMap[Int, Tab]()
|
||||||
|
|
||||||
/** Disconnect the ability to return stock back to the terminal
|
/** Disconnect the ability to return stock back to the terminal
|
||||||
* from the type of stock available from the terminal in general
|
* from the type of stock available from the terminal in general
|
||||||
|
|
@ -47,7 +42,7 @@ class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
||||||
*/
|
*/
|
||||||
private var sellEquipmentDefault: Boolean = false
|
private var sellEquipmentDefault: Boolean = false
|
||||||
|
|
||||||
def Tab: mutable.HashMap[Int, OrderTerminalDefinition.Tab] = tabs
|
def Tab: mutable.HashMap[Int, Tab] = tabs
|
||||||
|
|
||||||
def SellEquipmentByDefault: Boolean = sellEquipmentDefault
|
def SellEquipmentByDefault: Boolean = sellEquipmentDefault
|
||||||
|
|
||||||
|
|
@ -107,337 +102,6 @@ class OrderTerminalDefinition(objId: Int) extends TerminalDefinition(objId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
object OrderTerminalDefinition {
|
object OrderTerminalDefinition {
|
||||||
|
|
||||||
/**
|
|
||||||
* A basic tab outlining the specific type of stock available from this part of the terminal's interface.
|
|
||||||
* @see `ItemTransactionMessage`
|
|
||||||
*/
|
|
||||||
sealed trait Tab {
|
|
||||||
def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange
|
|
||||||
def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = Terminal.NoDeal()
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select an exo-suit to be worn by the player.
|
|
||||||
* @see `ExoSuitType`
|
|
||||||
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
|
|
||||||
*/
|
|
||||||
final case class ArmorPage(stock: Map[String, (ExoSuitType.Value, Int)]) extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
|
|
||||||
Terminal.BuyExosuit(suit, subtype)
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
msg.player.Actor ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An expanded form of the tab used to select an exo-suit to be worn by the player that also provides some equipment.
|
|
||||||
* @see `ExoSuitType`
|
|
||||||
* @see `Equipment`
|
|
||||||
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a tuple composed of an `ExoSuitType` value and a subtype value
|
|
||||||
* @param items the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a curried function that produces an `Equipment` object
|
|
||||||
*/
|
|
||||||
final case class ArmorWithAmmoPage(stock: Map[String, (ExoSuitType.Value, Int)], items: Map[String, () => Equipment])
|
|
||||||
extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some((suit: ExoSuitType.Value, subtype: Int)) =>
|
|
||||||
Terminal.BuyExosuit(suit, subtype)
|
|
||||||
case _ =>
|
|
||||||
items.get(msg.item_name) match {
|
|
||||||
case Some(item) =>
|
|
||||||
Terminal.BuyEquipment(item())
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
msg.response match {
|
|
||||||
case _: Terminal.BuyExosuit => msg.player.Actor ! msg
|
|
||||||
case _ => sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select a certification to be utilized by the player.
|
|
||||||
* Only certifications may be returned to the interface defined by this page.
|
|
||||||
*
|
|
||||||
* @see `CertificationType`
|
|
||||||
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a `CertificationType` value
|
|
||||||
*/
|
|
||||||
final case class CertificationPage(stock: Seq[Certification]) extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.find(_.name == msg.item_name) match {
|
|
||||||
case Some(cert: Certification) =>
|
|
||||||
Terminal.LearnCertification(cert)
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.find(_.name == msg.item_name) match {
|
|
||||||
case Some(cert: Certification) =>
|
|
||||||
Terminal.SellCertification(cert)
|
|
||||||
case None =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to produce an `Equipment` object to be used by the player.
|
|
||||||
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a curried function that produces an `Equipment` object
|
|
||||||
*/
|
|
||||||
final case class EquipmentPage(stock: Map[String, () => Equipment]) extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some(item) =>
|
|
||||||
Terminal.BuyEquipment(item())
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select an implant to be utilized by the player.
|
|
||||||
* A maximum of three implants can be obtained by any player at a time depending on the player's battle rank.
|
|
||||||
* Only implants may be returned to the interface defined by this page.
|
|
||||||
* @see `ImplantDefinition`
|
|
||||||
* @param stock the key is always a `String` value as defined from `ItemTransationMessage` data;
|
|
||||||
* the value is a `CertificationType` value
|
|
||||||
*/
|
|
||||||
final case class ImplantPage(stock: Map[String, ImplantDefinition]) extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some(implant: ImplantDefinition) =>
|
|
||||||
Terminal.LearnImplant(implant)
|
|
||||||
case None =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def Sell(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some(implant: ImplantDefinition) =>
|
|
||||||
Terminal.SellImplant(implant)
|
|
||||||
case None =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base class for "loadout" type tabs.
|
|
||||||
* Defines logic for enumerating items and entities that should be eliminated from being loaded.
|
|
||||||
* The method for filtering those excluded items, if applicable,
|
|
||||||
* and management of the resulting loadout object
|
|
||||||
* is the responsibility of the specific tab that is instantiated.
|
|
||||||
*/
|
|
||||||
abstract class LoadoutTab extends Tab {
|
|
||||||
private var contraband: Seq[Any] = Nil
|
|
||||||
|
|
||||||
def Exclude: Seq[Any] = contraband
|
|
||||||
|
|
||||||
def Exclude_=(equipment: Any): Seq[Any] = {
|
|
||||||
contraband = Seq(equipment)
|
|
||||||
Exclude
|
|
||||||
}
|
|
||||||
|
|
||||||
def Exclude_=(equipmentList: Seq[Any]): Seq[Any] = {
|
|
||||||
contraband = equipmentList
|
|
||||||
Exclude
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select which custom loadout the player is using.
|
|
||||||
* Player loadouts are defined by an exo-suit to be worn by the player
|
|
||||||
* and equipment in the holsters and the inventory.
|
|
||||||
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
|
|
||||||
* no extra information specific to this page is necessary.
|
|
||||||
* If an exo-suit type is considered excluded, the whole loadout is blocked.
|
|
||||||
* If the exclusion is written as a `Tuple` object `(A, B)`,
|
|
||||||
* `A` will be expected as an exo-suit type, and `B` will be expected as its subtype,
|
|
||||||
* and the pair must both match to block the whole loadout.
|
|
||||||
* If any of the player's inventory is considered excluded, only those items will be filtered.
|
|
||||||
* @see `ExoSuitType`
|
|
||||||
* @see `Equipment`
|
|
||||||
* @see `InfantryLoadout`
|
|
||||||
* @see `Loadout`
|
|
||||||
*/
|
|
||||||
//TODO block equipment by blocking ammunition type
|
|
||||||
final case class InfantryLoadoutPage() extends LoadoutTab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
player.avatar.loadouts(msg.unk1) match {
|
|
||||||
case Some(loadout: InfantryLoadout)
|
|
||||||
if !Exclude.contains(loadout.exosuit) && !Exclude.contains((loadout.exosuit, loadout.subtype)) =>
|
|
||||||
val holsters = loadout.visible_slots
|
|
||||||
.map(entry => {
|
|
||||||
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
|
|
||||||
val inventory = loadout.inventory
|
|
||||||
.map(entry => {
|
|
||||||
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
|
|
||||||
Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory)
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
msg.player.Actor ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select which custom loadout the player's vehicle is using.
|
|
||||||
* Vehicle loadouts are defined by a (superfluous) redefinition of the vehicle's mounted weapons
|
|
||||||
* and equipment in the trunk.
|
|
||||||
* In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts;
|
|
||||||
* no extra information specific to this page is necessary.
|
|
||||||
* If a vehicle type (by definition) is considered excluded, the whole loadout is blocked.
|
|
||||||
* If any of the vehicle's inventory is considered excluded, only those items will be filtered.
|
|
||||||
* @see `Equipment`
|
|
||||||
* @see `Loadout`
|
|
||||||
* @see `VehicleLoadout`
|
|
||||||
*/
|
|
||||||
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
player.avatar.loadouts(msg.unk1 + lineOffset) match {
|
|
||||||
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
|
|
||||||
val weapons = loadout.visible_slots
|
|
||||||
.map(entry => {
|
|
||||||
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
val inventory = loadout.inventory
|
|
||||||
.map(entry => {
|
|
||||||
InventoryItem(BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
.filterNot { entry => Exclude.contains(entry.obj.Definition) }
|
|
||||||
Terminal.VehicleLoadout(loadout.vehicle_definition, weapons, inventory)
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
val player = msg.player
|
|
||||||
player.Zone.GUID(player.avatar.vehicle) match {
|
|
||||||
case Some(vehicle: Vehicle) => vehicle.Actor ! msg
|
|
||||||
case _ => sender ! Terminal.TerminalMessage(player, msg.msg, Terminal.NoDeal())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab used to select a vehicle to be spawned for the player.
|
|
||||||
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
|
|
||||||
* and equipment in the trunk
|
|
||||||
* for the purpose of establishing default contents.
|
|
||||||
* @see `Equipment`
|
|
||||||
* @see `Loadout`
|
|
||||||
* @see `Vehicle`
|
|
||||||
* @see `VehicleLoadout`
|
|
||||||
*/
|
|
||||||
import net.psforever.objects.loadouts.{Loadout => Contents} //distinguish from Terminal.Loadout message
|
|
||||||
final case class VehiclePage(stock: Map[String, () => Vehicle], trunk: Map[String, Contents]) extends Tab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
stock.get(msg.item_name) match {
|
|
||||||
case Some(vehicle) =>
|
|
||||||
val (weapons, inventory) = trunk.get(msg.item_name) match {
|
|
||||||
case Some(loadout: VehicleLoadout) =>
|
|
||||||
(
|
|
||||||
loadout.visible_slots.map(entry => {
|
|
||||||
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
}),
|
|
||||||
loadout.inventory.map(entry => {
|
|
||||||
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
case _ =>
|
|
||||||
(List.empty, List.empty)
|
|
||||||
}
|
|
||||||
Terminal.BuyVehicle(vehicle(), weapons, inventory)
|
|
||||||
case None =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The special page used by the `bfr_terminal` to select a vehicle to be spawned
|
|
||||||
* based on the player's previous loadouts for battleframe vehicles.
|
|
||||||
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
|
|
||||||
* and equipment in the trunk.
|
|
||||||
* @see `Equipment`
|
|
||||||
* @see `Loadout`
|
|
||||||
* @see `Vehicle`
|
|
||||||
* @see `VehicleLoadout`
|
|
||||||
*/
|
|
||||||
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
|
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
|
||||||
player.avatar.loadouts(msg.unk1 + 15) match {
|
|
||||||
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
|
|
||||||
vehicles.get(loadout.vehicle_definition.Name) match {
|
|
||||||
case Some(vehicle) =>
|
|
||||||
val weapons = loadout.visible_slots.map(entry => {
|
|
||||||
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
val inventory = loadout.inventory.map(entry => {
|
|
||||||
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
|
|
||||||
})
|
|
||||||
Terminal.BuyVehicle(vehicle(), weapons, inventory)
|
|
||||||
case None =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
|
|
||||||
case _ =>
|
|
||||||
Terminal.NoDeal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
|
|
||||||
sender ! msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assemble some logic for a provided object.
|
* Assemble some logic for a provided object.
|
||||||
* @param obj an `Amenity` object;
|
* @param obj an `Amenity` object;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
(projectile.fire_mode, projectile.profile.Charging) match {
|
||||||
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
|
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
|
||||||
val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f))
|
val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f))
|
||||||
cause.damageModel.DamageUsing(info.min) + (damage * chargeQuality).toInt
|
val min = cause.damageModel.DamageUsing(info.min)
|
||||||
|
val range = if (damage > min) { damage - min } else { min - damage }
|
||||||
|
min + (range * chargeQuality).toInt
|
||||||
case _ =>
|
case _ =>
|
||||||
damage
|
damage
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import scala.util.Try
|
||||||
import akka.actor.typed
|
import akka.actor.typed
|
||||||
import net.psforever.actors.session.AvatarActor
|
import net.psforever.actors.session.AvatarActor
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
|
import net.psforever.actors.zone.building.WarpGateLogic
|
||||||
import net.psforever.objects.avatar.Avatar
|
import net.psforever.objects.avatar.Avatar
|
||||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||||
import net.psforever.objects.guid.pool.NumberPool
|
import net.psforever.objects.guid.pool.NumberPool
|
||||||
|
|
@ -51,6 +52,9 @@ import net.psforever.objects.vital.Vitality
|
||||||
import net.psforever.objects.zones.blockmap.BlockMap
|
import net.psforever.objects.zones.blockmap.BlockMap
|
||||||
import net.psforever.services.Service
|
import net.psforever.services.Service
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.concurrent.{Future, Promise}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A server object representing the one-landmass planets as well as the individual subterranean caverns.<br>
|
* A server object representing the one-landmass planets as well as the individual subterranean caverns.<br>
|
||||||
* <br>
|
* <br>
|
||||||
|
|
@ -168,6 +172,18 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
||||||
*/
|
*/
|
||||||
private var vehicleEvents: ActorRef = Default.Actor
|
private var vehicleEvents: ActorRef = Default.Actor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the zone has completed initializing, fulfill this promise.
|
||||||
|
* @see `init(ActorContext)`
|
||||||
|
*/
|
||||||
|
private var zoneInitialized: Promise[Boolean] = Promise[Boolean]()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the zone has completed initializing, this will be the future.
|
||||||
|
* @see `init(ActorContext)`
|
||||||
|
*/
|
||||||
|
def ZoneInitialized(): Future[Boolean] = zoneInitialized.future
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establish the basic accessible conditions necessary for a functional `Zone`.<br>
|
* Establish the basic accessible conditions necessary for a functional `Zone`.<br>
|
||||||
* <br>
|
* <br>
|
||||||
|
|
@ -213,8 +229,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
||||||
AssignAmenities()
|
AssignAmenities()
|
||||||
CreateSpawnGroups()
|
CreateSpawnGroups()
|
||||||
PopulateBlockMap()
|
PopulateBlockMap()
|
||||||
|
|
||||||
validate()
|
validate()
|
||||||
|
|
||||||
|
zoneInitialized.success(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,7 +375,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
||||||
case (building, _) =>
|
case (building, _) =>
|
||||||
building match {
|
building match {
|
||||||
case warpGate: WarpGate =>
|
case warpGate: WarpGate =>
|
||||||
warpGate.Faction == faction || warpGate.Faction == PlanetSideEmpire.NEUTRAL || warpGate.Broadcast
|
warpGate.Faction == faction || warpGate.Broadcast(faction)
|
||||||
case _ =>
|
case _ =>
|
||||||
building.Faction == faction
|
building.Faction == faction
|
||||||
}
|
}
|
||||||
|
|
@ -570,6 +587,20 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
||||||
lattice
|
lattice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def AddIntercontinentalLatticeLink(bldgA: Building, bldgB: Building): Graph[Building, UnDiEdge] = {
|
||||||
|
if ((this eq bldgA.Zone) && (bldgA.Zone ne bldgB.Zone)) {
|
||||||
|
lattice ++= Set(bldgA ~ bldgB)
|
||||||
|
}
|
||||||
|
Lattice
|
||||||
|
}
|
||||||
|
|
||||||
|
def RemoveIntercontinentalLatticeLink(bldgA: Building, bldgB: Building): Graph[Building, UnDiEdge] = {
|
||||||
|
if ((this eq bldgA.Zone) && (bldgA.Zone ne bldgB.Zone)) {
|
||||||
|
lattice --= Set(bldgA ~ bldgB)
|
||||||
|
}
|
||||||
|
Lattice
|
||||||
|
}
|
||||||
|
|
||||||
def zipLinePaths: List[ZipLinePath] = {
|
def zipLinePaths: List[ZipLinePath] = {
|
||||||
map.zipLinePaths
|
map.zipLinePaths
|
||||||
}
|
}
|
||||||
|
|
@ -674,15 +705,19 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private def MakeLattice(): Unit = {
|
private def MakeLattice(): Unit = {
|
||||||
lattice ++= map.latticeLink.map {
|
lattice ++= map.latticeLink
|
||||||
case (source, target) =>
|
.filterNot {
|
||||||
val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match {
|
case (a, _) => a.contains("/") //ignore intercontinental lattice connections
|
||||||
case (Some(sBuilding), Some(tBuilding)) => (sBuilding, tBuilding)
|
}
|
||||||
case _ =>
|
.map {
|
||||||
throw new NoSuchElementException(s"Can't create lattice link between $source $target. Source is missing")
|
case (source, target) =>
|
||||||
}
|
val (sourceBuilding, targetBuilding) = (Building(source), Building(target)) match {
|
||||||
sourceBuilding ~ targetBuilding
|
case (Some(sBuilding), Some(tBuilding)) => (sBuilding, tBuilding)
|
||||||
}
|
case _ =>
|
||||||
|
throw new NoSuchElementException(s"Zone $id - can't create lattice link between $source and $target.")
|
||||||
|
}
|
||||||
|
sourceBuilding ~ targetBuilding
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def CreateSpawnGroups(): Unit = {
|
private def CreateSpawnGroups(): Unit = {
|
||||||
|
|
@ -1109,6 +1144,105 @@ object Zone {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting from an overworld zone facility,
|
||||||
|
* find a lattice connected cavern facility that is the same faction as this starting building.
|
||||||
|
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
|
||||||
|
* do not let the search escape the current zone into another.
|
||||||
|
* If we start in a cavern zone, do not continue a fruitless search;
|
||||||
|
* just fail.
|
||||||
|
* @return the discovered faction-aligned cavern facility
|
||||||
|
*/
|
||||||
|
def findConnectedCavernFacility(building: Building): Option[Building] = {
|
||||||
|
if (building.Zone.map.cavern) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
val neighbors = building.AllNeighbours.getOrElse(Set.empty[Building]).toList
|
||||||
|
recursiveFindConnectedCavernFacility(building.Faction, neighbors.headOption, neighbors.drop(1), Set(building.MapId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting from an overworld zone facility,
|
||||||
|
* find a lattice connected cavern facility that is the same faction as this starting building.
|
||||||
|
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
|
||||||
|
* do not let the search escape the current zone into another.
|
||||||
|
* @param currBuilding the proposed current facility to check
|
||||||
|
* @param nextNeighbors the facilities that are yet to be searched
|
||||||
|
* @param visitedNeighbors the facilities that have been searched already
|
||||||
|
* @return the discovered faction-aligned cavern facility
|
||||||
|
*/
|
||||||
|
@tailrec
|
||||||
|
private def recursiveFindConnectedCavernFacility(
|
||||||
|
sampleFaction: PlanetSideEmpire.Value,
|
||||||
|
currBuilding: Option[Building],
|
||||||
|
nextNeighbors: List[Building],
|
||||||
|
visitedNeighbors: Set[Int]
|
||||||
|
): Option[Building] = {
|
||||||
|
if(currBuilding.isEmpty) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
val building = currBuilding.head
|
||||||
|
if (!visitedNeighbors.contains(building.MapId)
|
||||||
|
&& (building match {
|
||||||
|
case wg: WarpGate => wg.Faction == sampleFaction || wg.Broadcast(sampleFaction)
|
||||||
|
case _ => building.Faction == sampleFaction
|
||||||
|
})
|
||||||
|
&& !building.CaptureTerminalIsHacked
|
||||||
|
&& building.NtuLevel > 0
|
||||||
|
&& (building.Generator match {
|
||||||
|
case Some(o) => o.Condition != PlanetSideGeneratorState.Destroyed
|
||||||
|
case _ => true
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
(building match {
|
||||||
|
case wg: WarpGate => traverseWarpGateInSearchOfOwnedCavernFaciity(sampleFaction, wg)
|
||||||
|
case _ => None
|
||||||
|
}) match {
|
||||||
|
case out @ Some(_) =>
|
||||||
|
out
|
||||||
|
case _ =>
|
||||||
|
val newVisitedNeighbors = visitedNeighbors ++ Set(building.MapId)
|
||||||
|
val newNeighbors = nextNeighbors ++ building.AllNeighbours
|
||||||
|
.getOrElse(Set.empty[Building])
|
||||||
|
.toList
|
||||||
|
.filterNot { b => newVisitedNeighbors.contains(b.MapId) }
|
||||||
|
recursiveFindConnectedCavernFacility(
|
||||||
|
sampleFaction,
|
||||||
|
newNeighbors.headOption,
|
||||||
|
newNeighbors.drop(1),
|
||||||
|
newVisitedNeighbors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recursiveFindConnectedCavernFacility(
|
||||||
|
sampleFaction,
|
||||||
|
nextNeighbors.headOption,
|
||||||
|
nextNeighbors.drop(1),
|
||||||
|
visitedNeighbors ++ Set(building.MapId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace the extended neighborhood of the warp gate to a cavern facility that has the same faction affinity.
|
||||||
|
* @param faction the faction that all connected factions must have affinity with
|
||||||
|
* @param target the warp gate from which to conduct a local search
|
||||||
|
* @return if discovered, the first faction affiliated facility in a connected cavern
|
||||||
|
*/
|
||||||
|
private def traverseWarpGateInSearchOfOwnedCavernFaciity(
|
||||||
|
faction: PlanetSideEmpire.Value,
|
||||||
|
target: WarpGate
|
||||||
|
): Option[Building] = {
|
||||||
|
WarpGateLogic.findNeighborhoodWarpGate(target.Neighbours.getOrElse(Nil)) match {
|
||||||
|
case Some(gate) if gate.Zone.map.cavern =>
|
||||||
|
WarpGateLogic.findNeighborhoodNormalBuilding(gate.Neighbours(faction).getOrElse(Nil))
|
||||||
|
case _ =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
|
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
|
||||||
* and informs those entities that they have affected by the aforementioned damage.
|
* and informs those entities that they have affected by the aforementioned damage.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import akka.actor.{Actor, ActorRef, Props}
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
|
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
|
||||||
import net.psforever.objects.{Default, Player}
|
import net.psforever.objects.{Default, Player}
|
||||||
|
import net.psforever.types.Vector3
|
||||||
|
|
||||||
import scala.collection.concurrent.TrieMap
|
import scala.collection.concurrent.TrieMap
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
@ -37,6 +38,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
||||||
tplayer.Zone = Zone.Nowhere
|
tplayer.Zone = Zone.Nowhere
|
||||||
PlayerLeave(tplayer)
|
PlayerLeave(tplayer)
|
||||||
if (tplayer.VehicleSeated.isEmpty) {
|
if (tplayer.VehicleSeated.isEmpty) {
|
||||||
|
tplayer.Position = Vector3.Zero
|
||||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||||
}
|
}
|
||||||
sender() ! Zone.Population.PlayerHasLeft(zone, player)
|
sender() ! Zone.Population.PlayerHasLeft(zone, player)
|
||||||
|
|
@ -70,6 +72,7 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
||||||
case Some(tplayer) =>
|
case Some(tplayer) =>
|
||||||
PlayerLeave(tplayer)
|
PlayerLeave(tplayer)
|
||||||
if (tplayer.VehicleSeated.isEmpty) {
|
if (tplayer.VehicleSeated.isEmpty) {
|
||||||
|
tplayer.Position = Vector3.Zero
|
||||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||||
}
|
}
|
||||||
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))
|
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package net.psforever.objects.zones
|
||||||
import akka.actor.Actor
|
import akka.actor.Actor
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.{Default, Vehicle}
|
import net.psforever.objects.{Default, Vehicle}
|
||||||
|
import net.psforever.types.Vector3
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
@ -52,6 +53,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
|
||||||
case Some(index) =>
|
case Some(index) =>
|
||||||
vehicleList.remove(index)
|
vehicleList.remove(index)
|
||||||
vehicle.Definition.Uninitialize(vehicle, context)
|
vehicle.Definition.Uninitialize(vehicle, context)
|
||||||
|
vehicle.Position = Vector3.Zero
|
||||||
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
|
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
|
||||||
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
|
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
|
||||||
case None => ;
|
case None => ;
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,12 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
|
||||||
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
|
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
|
||||||
*/
|
*/
|
||||||
def sector(p: Vector3, range: Float): SectorPopulation = {
|
def sector(p: Vector3, range: Float): SectorPopulation = {
|
||||||
BlockMap.quickToSectorGroup(range, BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } )
|
val indices = BlockMap.findSectorIndices(blockMap = this, p, range)
|
||||||
|
if (indices.max < blocks.size) {
|
||||||
|
BlockMap.quickToSectorGroup(range, indices.map { blocks } )
|
||||||
|
} else {
|
||||||
|
SectorGroup(Nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -556,7 +556,7 @@ object GamePacketOpcode extends Enumeration {
|
||||||
case 0xd3 => game.ComponentDamageMessage.decode
|
case 0xd3 => game.ComponentDamageMessage.decode
|
||||||
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
|
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
|
||||||
case 0xd5 => game.PropertyOverrideMessage.decode
|
case 0xd5 => game.PropertyOverrideMessage.decode
|
||||||
case 0xd6 => noDecoder(WarpgateLinkOverrideMessage)
|
case 0xd6 => game.WarpgateLinkOverrideMessage.decode
|
||||||
case 0xd7 => noDecoder(EmpireBenefitsMessage)
|
case 0xd7 => noDecoder(EmpireBenefitsMessage)
|
||||||
// 0xd8
|
// 0xd8
|
||||||
case 0xd8 => noDecoder(ForceEmpireMessage)
|
case 0xd8 => noDecoder(ForceEmpireMessage)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package net.psforever.packet.game
|
package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||||
|
import net.psforever.types.PlanetSideEmpire
|
||||||
import scodec.Codec
|
import scodec.Codec
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
|
|
||||||
|
|
@ -21,14 +22,40 @@ import scodec.codecs._
|
||||||
* @param nc players belonging to the New Conglomerate interact with this warp gate as a "broadcast gate"
|
* @param nc players belonging to the New Conglomerate interact with this warp gate as a "broadcast gate"
|
||||||
* @param vs players belonging to the Vanu Sovereignty interact with this warp gate as a "broadcast gate"
|
* @param vs players belonging to the Vanu Sovereignty interact with this warp gate as a "broadcast gate"
|
||||||
*/
|
*/
|
||||||
final case class BroadcastWarpgateUpdateMessage(zone_id: Int, building_id: Int, tr: Boolean, nc: Boolean, vs: Boolean)
|
final case class BroadcastWarpgateUpdateMessage(
|
||||||
extends PlanetSideGamePacket {
|
zone_id: Int,
|
||||||
|
building_id: Int,
|
||||||
|
tr: Boolean,
|
||||||
|
nc: Boolean,
|
||||||
|
vs: Boolean
|
||||||
|
) extends PlanetSideGamePacket {
|
||||||
type Packet = BroadcastWarpgateUpdateMessage
|
type Packet = BroadcastWarpgateUpdateMessage
|
||||||
def opcode = GamePacketOpcode.BroadcastWarpgateUpdateMessage
|
def opcode = GamePacketOpcode.BroadcastWarpgateUpdateMessage
|
||||||
def encode = BroadcastWarpgateUpdateMessage.encode(this)
|
def encode = BroadcastWarpgateUpdateMessage.encode(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
object BroadcastWarpgateUpdateMessage extends Marshallable[BroadcastWarpgateUpdateMessage] {
|
object BroadcastWarpgateUpdateMessage extends Marshallable[BroadcastWarpgateUpdateMessage] {
|
||||||
|
def apply(
|
||||||
|
zoneId: Int,
|
||||||
|
buildingId: Int,
|
||||||
|
faction: PlanetSideEmpire.Value
|
||||||
|
): BroadcastWarpgateUpdateMessage = {
|
||||||
|
BroadcastWarpgateUpdateMessage(zoneId, buildingId, Set(faction))
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
zoneId: Int,
|
||||||
|
buildingId: Int,
|
||||||
|
factions: Set[PlanetSideEmpire.Value]
|
||||||
|
): BroadcastWarpgateUpdateMessage = {
|
||||||
|
val f = {
|
||||||
|
val out = Array.fill(PlanetSideEmpire.values.size)(false)
|
||||||
|
factions.map(_.id).foreach { i => out.update(i, true) }
|
||||||
|
out
|
||||||
|
}
|
||||||
|
BroadcastWarpgateUpdateMessage(zoneId, buildingId, f(0), f(1), f(2))
|
||||||
|
}
|
||||||
|
|
||||||
implicit val codec: Codec[BroadcastWarpgateUpdateMessage] = (
|
implicit val codec: Codec[BroadcastWarpgateUpdateMessage] = (
|
||||||
("zone_id" | uint16L) ::
|
("zone_id" | uint16L) ::
|
||||||
("building_id" | uint16L) ::
|
("building_id" | uint16L) ::
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.packet.game
|
package net.psforever.packet.game
|
||||||
|
|
||||||
|
import enumeratum.values.IntEnum
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState}
|
import net.psforever.types._
|
||||||
import scodec.{Attempt, Codec, Err}
|
import scodec.{Attempt, Codec, Err}
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
import shapeless.{::, HNil}
|
||||||
|
|
@ -47,28 +48,7 @@ final case class Additional3(unk1: Boolean, unk2: Int)
|
||||||
* A parameter that is not applicable for a given asset, e.g., NTU for a field tower, will be ignored.
|
* A parameter that is not applicable for a given asset, e.g., NTU for a field tower, will be ignored.
|
||||||
* A collision between some parameters can occur.
|
* A collision between some parameters can occur.
|
||||||
* For example, if `is_hacking` is `false`, the other hacking fields are considered invalid.
|
* For example, if `is_hacking` is `false`, the other hacking fields are considered invalid.
|
||||||
* If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.<br>
|
* If `is_hacking` is `true` but the hacking empire is also the owning empire, the `is_hacking` state is invalid.
|
||||||
* <br>
|
|
||||||
* Lattice benefits: (stackable)<br>
|
|
||||||
* `
|
|
||||||
* 00 - None<br>
|
|
||||||
* 01 - Amp Station<br>
|
|
||||||
* 02 - Dropship Center<br>
|
|
||||||
* 04 - Bio Laboratory<br>
|
|
||||||
* 08 - Interlink Facility<br>
|
|
||||||
* 16 - Technology Plant<br>
|
|
||||||
* `
|
|
||||||
* <br>
|
|
||||||
* Cavern benefits: (stackable)<br>
|
|
||||||
* `
|
|
||||||
* 000 - None<br>
|
|
||||||
* 004 - Speed Module<br>
|
|
||||||
* 008 - Shield Module<br>
|
|
||||||
* 016 - Vehicle Module<br>
|
|
||||||
* 032 - Equipment Module<br>
|
|
||||||
* 064 - Health Module<br>
|
|
||||||
* 128 - Pain Module<br>
|
|
||||||
* `
|
|
||||||
* @param continent_id the continent (zone)
|
* @param continent_id the continent (zone)
|
||||||
* @param building_map_id the map id of this building from the MPO files
|
* @param building_map_id the map id of this building from the MPO files
|
||||||
* @param ntu_level if the building has a silo, the amount of NTU in that silo;
|
* @param ntu_level if the building has a silo, the amount of NTU in that silo;
|
||||||
|
|
@ -87,9 +67,9 @@ final case class Additional3(unk1: Boolean, unk2: Int)
|
||||||
* @param force_dome_active if the building is a capitol facility, whether the force dome is active
|
* @param force_dome_active if the building is a capitol facility, whether the force dome is active
|
||||||
* @param lattice_benefit the benefits from other Lattice-linked bases does this building possess
|
* @param lattice_benefit the benefits from other Lattice-linked bases does this building possess
|
||||||
* @param cavern_benefit cavern benefits;
|
* @param cavern_benefit cavern benefits;
|
||||||
* any non-zero value will cause the cavern module icon (yellow) to appear;
|
* any non-zero value will cause the cavern module icon (yellow) to appear;
|
||||||
* proper module values cause the cavern module icon to render green;
|
* proper module values cause the cavern module icon to render green;
|
||||||
* all benefits will report as due to a "Cavern Lock"
|
* all benefits will report as due to a "Cavern Lock"
|
||||||
* @param unk4 na
|
* @param unk4 na
|
||||||
* @param unk5 na
|
* @param unk5 na
|
||||||
* @param unk6 na
|
* @param unk6 na
|
||||||
|
|
@ -112,8 +92,8 @@ final case class BuildingInfoUpdateMessage(
|
||||||
generator_state: PlanetSideGeneratorState.Value,
|
generator_state: PlanetSideGeneratorState.Value,
|
||||||
spawn_tubes_normal: Boolean,
|
spawn_tubes_normal: Boolean,
|
||||||
force_dome_active: Boolean,
|
force_dome_active: Boolean,
|
||||||
lattice_benefit: Int,
|
lattice_benefit: Set[LatticeBenefit],
|
||||||
cavern_benefit: Int,
|
cavern_benefit: Set[CavernBenefit],
|
||||||
unk4: List[Additional2],
|
unk4: List[Additional2],
|
||||||
unk5: Long,
|
unk5: Long,
|
||||||
unk6: Boolean,
|
unk6: Boolean,
|
||||||
|
|
@ -128,7 +108,6 @@ final case class BuildingInfoUpdateMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] {
|
object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `Codec` for a set of additional fields.
|
* A `Codec` for a set of additional fields.
|
||||||
*/
|
*/
|
||||||
|
|
@ -154,6 +133,45 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
|
||||||
("unk2" | uint2L)
|
("unk2" | uint2L)
|
||||||
).as[Additional3]
|
).as[Additional3]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `Codec` for the benefits tallies
|
||||||
|
* transforming between numeric value and a set of enum values.
|
||||||
|
* The type of benefit is capable of being passed into the function.
|
||||||
|
* @param bits number of bits for this field
|
||||||
|
* @param objClass the benefits enumeration
|
||||||
|
* @tparam T the type of benefit in the enumeration (passive type)
|
||||||
|
* @return a `Codec` for the benefits tallies
|
||||||
|
*/
|
||||||
|
private def benefitCodecFunc[T <: CaptureBenefit](bits: Int, objClass: IntEnum[T]): Codec[Set[T]] = {
|
||||||
|
assert(
|
||||||
|
math.pow(2, bits) >= objClass.values.maxBy(data => data.value).value,
|
||||||
|
s"BuildingInfoUpdateMessage - $bits is not enough bits to represent ${objClass.getClass().getSimpleName()}"
|
||||||
|
)
|
||||||
|
uintL(bits).xmap[Set[T]](
|
||||||
|
{
|
||||||
|
case 0 =>
|
||||||
|
Set(objClass.values.find(_.value == 0).get)
|
||||||
|
case n =>
|
||||||
|
val values = objClass
|
||||||
|
.values
|
||||||
|
.sortBy(_.value)(Ordering.Int.reverse)
|
||||||
|
.dropRight(1) //drop value == 0
|
||||||
|
var curr = n
|
||||||
|
values
|
||||||
|
.collect {
|
||||||
|
case benefit if benefit.value <= curr =>
|
||||||
|
curr = curr - benefit.value
|
||||||
|
benefit
|
||||||
|
}.toSet
|
||||||
|
},
|
||||||
|
benefits => benefits.foldLeft[Int](0)(_ + _.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val latticeBenefitCodec: Codec[Set[LatticeBenefit]] = benefitCodecFunc(bits = 5, LatticeBenefit)
|
||||||
|
|
||||||
|
private val cavernBenefitCodec: Codec[Set[CavernBenefit]] = benefitCodecFunc(bits = 10, CavernBenefit)
|
||||||
|
|
||||||
implicit val codec: Codec[BuildingInfoUpdateMessage] = (
|
implicit val codec: Codec[BuildingInfoUpdateMessage] = (
|
||||||
("continent_id" | uint16L) ::
|
("continent_id" | uint16L) ::
|
||||||
("building_id" | uint16L) ::
|
("building_id" | uint16L) ::
|
||||||
|
|
@ -163,17 +181,17 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
|
||||||
("hack_time_remaining" | uint32L) ::
|
("hack_time_remaining" | uint32L) ::
|
||||||
("empire_own" | PlanetSideEmpire.codec) ::
|
("empire_own" | PlanetSideEmpire.codec) ::
|
||||||
(("unk1" | uint32L) >>:~ { unk1 =>
|
(("unk1" | uint32L) >>:~ { unk1 =>
|
||||||
conditional(unk1 != 0L, "unk1x" | additional1_codec) ::
|
conditional(unk1 != 0L, codec = "unk1x" | additional1_codec) ::
|
||||||
("generator_state" | PlanetSideGeneratorState.codec) ::
|
("generator_state" | PlanetSideGeneratorState.codec) ::
|
||||||
("spawn_tubes_normal" | bool) ::
|
("spawn_tubes_normal" | bool) ::
|
||||||
("force_dome_active" | bool) ::
|
("force_dome_active" | bool) ::
|
||||||
("lattice_benefit" | uintL(5)) ::
|
("lattice_benefit" | latticeBenefitCodec) ::
|
||||||
("cavern_benefit" | uintL(10)) ::
|
("cavern_benefit" | cavernBenefitCodec) ::
|
||||||
("unk4" | listOfN(uint4L, additional2_codec)) ::
|
("unk4" | listOfN(uint4L, additional2_codec)) ::
|
||||||
("unk5" | uint32L) ::
|
("unk5" | uint32L) ::
|
||||||
("unk6" | bool) ::
|
("unk6" | bool) ::
|
||||||
(("unk7" | uint4L) >>:~ { unk7 =>
|
(("unk7" | uint4L) >>:~ { unk7 =>
|
||||||
conditional(unk7 != 8, "unk7x" | additional3_codec) ::
|
conditional(unk7 != 8, codec = "unk7x" | additional3_codec) ::
|
||||||
("boost_spawn_pain" | bool) ::
|
("boost_spawn_pain" | bool) ::
|
||||||
("boost_generator_pain" | bool)
|
("boost_generator_pain" | bool)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ final case class ChatMsg(
|
||||||
}
|
}
|
||||||
|
|
||||||
object ChatMsg extends Marshallable[ChatMsg] {
|
object ChatMsg extends Marshallable[ChatMsg] {
|
||||||
|
def apply(messageType: ChatMessageType, contents: String): ChatMsg =
|
||||||
|
ChatMsg(messageType, wideContents=false, recipient="", contents, note=None)
|
||||||
|
|
||||||
implicit val codec: Codec[ChatMsg] = (("messagetype" | ChatMessageType.codec) >>:~ { messagetype_value =>
|
implicit val codec: Codec[ChatMsg] = (("messagetype" | ChatMessageType.codec) >>:~ { messagetype_value =>
|
||||||
(("has_wide_contents" | bool) >>:~ { isWide =>
|
(("has_wide_contents" | bool) >>:~ { isWide =>
|
||||||
("recipient" | PacketHelpers.encodedWideStringAligned(7)) ::
|
("recipient" | PacketHelpers.encodedWideStringAligned(7)) ::
|
||||||
|
|
|
||||||
|
|
@ -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
|
final val portable_manned_turret_vs = 688
|
||||||
//projectiles
|
//projectiles
|
||||||
final val aphelion_plasma_cloud = 96
|
final val aphelion_plasma_cloud = 96
|
||||||
|
final val aphelion_starfire_projectile = 108
|
||||||
final val flamethrower_fire_cloud = 301
|
final val flamethrower_fire_cloud = 301
|
||||||
final val hunter_seeker_missile_projectile = 405 //phoenix projectile
|
final val hunter_seeker_missile_projectile = 405 //phoenix projectile
|
||||||
final val maelstrom_grenade_damager = 464
|
final val maelstrom_grenade_damager = 464
|
||||||
|
|
@ -327,6 +328,7 @@ object ObjectClass {
|
||||||
final val meteor_projectile_medium = 548
|
final val meteor_projectile_medium = 548
|
||||||
final val meteor_projectile_small = 549
|
final val meteor_projectile_small = 549
|
||||||
final val peregrine_particle_cannon_radiation_cloud = 655
|
final val peregrine_particle_cannon_radiation_cloud = 655
|
||||||
|
final val peregrine_sparrow_projectile = 661
|
||||||
final val phoenix_missile_guided_projectile = 675 //decimator projectile
|
final val phoenix_missile_guided_projectile = 675 //decimator projectile
|
||||||
final val oicw_little_buddy = 601 //scorpion projectile's projectiles
|
final val oicw_little_buddy = 601 //scorpion projectile's projectiles
|
||||||
final val oicw_projectile = 602 //scorpion projectile
|
final val oicw_projectile = 602 //scorpion projectile
|
||||||
|
|
@ -1231,6 +1233,7 @@ object ObjectClass {
|
||||||
case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable")
|
case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable")
|
||||||
//projectiles
|
//projectiles
|
||||||
case ObjectClass.aphelion_plasma_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
|
case ObjectClass.aphelion_plasma_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
|
||||||
|
case ObjectClass.aphelion_starfire_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||||
case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||||
case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor")
|
case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor")
|
||||||
case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor")
|
case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor")
|
||||||
|
|
@ -1240,6 +1243,7 @@ object ObjectClass {
|
||||||
case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor")
|
case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor")
|
||||||
case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor")
|
case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor")
|
||||||
case ObjectClass.peregrine_particle_cannon_radiation_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
|
case ObjectClass.peregrine_particle_cannon_radiation_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud")
|
||||||
|
case ObjectClass.peregrine_sparrow_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||||
case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||||
case ObjectClass.oicw_little_buddy => ConstructorData(LittleBuddyProjectileData.codec, "projectile")
|
case ObjectClass.oicw_little_buddy => ConstructorData(LittleBuddyProjectileData.codec, "projectile")
|
||||||
case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile")
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@ object RemoteProjectiles {
|
||||||
final case object Meteor extends Data(0, 32)
|
final case object Meteor extends Data(0, 32)
|
||||||
final case object Wasp extends Data(0, 208)
|
final case object Wasp extends Data(0, 208)
|
||||||
final case object Sparrow extends Data(13107, 187)
|
final case object Sparrow extends Data(13107, 187)
|
||||||
|
final case object PeregrineSparrow extends Data(13107, 187)
|
||||||
final case object OICW extends Data(13107, 195)
|
final case object OICW extends Data(13107, 195)
|
||||||
final case object Striker extends Data(26214, 134)
|
final case object Striker extends Data(26214, 134)
|
||||||
final case object HunterSeeker extends Data(39577, 201)
|
final case object HunterSeeker extends Data(39577, 201)
|
||||||
final case object Starfire extends Data(39577, 249)
|
final case object Starfire extends Data(39577, 249)
|
||||||
|
final case object AphelionStarfire extends Data(39577, 249)
|
||||||
|
|
||||||
//the oicw_little_buddy is handled by its own transcoder
|
//the oicw_little_buddy is handled by its own transcoder
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.Avatar
|
import net.psforever.objects.avatar.Avatar
|
||||||
import net.psforever.objects.{Player, SpawnPoint, Vehicle}
|
import net.psforever.objects.{Player, SpawnPoint, Vehicle}
|
||||||
import net.psforever.objects.serverobject.structures.Building
|
import net.psforever.objects.serverobject.structures.{Building, WarpGate}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.game.DroppodError
|
import net.psforever.packet.game.DroppodError
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
|
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
|
||||||
|
|
@ -14,7 +14,8 @@ import net.psforever.util.Config
|
||||||
import net.psforever.zones.Zones
|
import net.psforever.zones.Zones
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.util.Random
|
import scala.concurrent.Future
|
||||||
|
import scala.util.{Random, Success}
|
||||||
|
|
||||||
object InterstellarClusterService {
|
object InterstellarClusterService {
|
||||||
val InterstellarClusterServiceKey: ServiceKey[Command] =
|
val InterstellarClusterServiceKey: ServiceKey[Command] =
|
||||||
|
|
@ -51,6 +52,8 @@ object InterstellarClusterService {
|
||||||
zoneNumber: Int,
|
zoneNumber: Int,
|
||||||
player: Player,
|
player: Player,
|
||||||
target: PlanetSideGUID,
|
target: PlanetSideGUID,
|
||||||
|
fromZoneNumber: Int,
|
||||||
|
fromGateGuid: PlanetSideGUID,
|
||||||
replyTo: ActorRef[SpawnPointResponse]
|
replyTo: ActorRef[SpawnPointResponse]
|
||||||
) extends Command
|
) extends Command
|
||||||
|
|
||||||
|
|
@ -81,27 +84,50 @@ object InterstellarClusterService {
|
||||||
replyTo: ActorRef[DroppodLaunchExchange]
|
replyTo: ActorRef[DroppodLaunchExchange]
|
||||||
) extends Command
|
) extends Command
|
||||||
|
|
||||||
|
final case class CavernRotation(msg: CavernRotationService.Command) extends Command
|
||||||
|
|
||||||
trait DroppodLaunchExchange
|
trait DroppodLaunchExchange
|
||||||
|
|
||||||
final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange
|
final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange
|
||||||
|
|
||||||
final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange
|
final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange
|
||||||
|
|
||||||
|
private case class ReceptionistListing(listing: Receptionist.Listing) extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone])
|
class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone])
|
||||||
extends AbstractBehavior[InterstellarClusterService.Command](context) {
|
extends AbstractBehavior[InterstellarClusterService.Command](context) {
|
||||||
|
|
||||||
import InterstellarClusterService._
|
import InterstellarClusterService._
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
|
var intercontinentalSetup: Boolean = false
|
||||||
|
var cavernRotation: Option[ActorRef[CavernRotationService.Command]] = None
|
||||||
|
|
||||||
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = mutable.Map(
|
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = {
|
||||||
_zones.map {
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
zone =>
|
//setup the callback upon each successful result
|
||||||
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}")
|
val zoneLoadedList = _zones.map { _.ZoneInitialized() }
|
||||||
(zone.id, (zoneActor, zone))
|
val continentLinkFunc: ()=>Unit = MakeIntercontinentalLattice(
|
||||||
}.toSeq: _*
|
zoneLoadedList.toList,
|
||||||
)
|
context.system.receptionist,
|
||||||
|
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
|
||||||
|
)
|
||||||
|
zoneLoadedList.foreach {
|
||||||
|
_.onComplete({
|
||||||
|
case Success(true) => continentLinkFunc()
|
||||||
|
case _ => //log.error("")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//construct the zones, resulting in the callback
|
||||||
|
mutable.Map(
|
||||||
|
_zones.map {
|
||||||
|
zone =>
|
||||||
|
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}")
|
||||||
|
(zone.id, (zoneActor, zone))
|
||||||
|
}.toSeq: _*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val zones: Iterable[Zone] = zoneActors.map {
|
val zones: Iterable[Zone] = zoneActors.map {
|
||||||
case (_, (_, zone: Zone)) => zone
|
case (_, (_, zone: Zone)) => zone
|
||||||
|
|
@ -109,8 +135,21 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
|
|
||||||
override def onMessage(msg: Command): Behavior[Command] = {
|
override def onMessage(msg: Command): Behavior[Command] = {
|
||||||
msg match {
|
msg match {
|
||||||
|
case ReceptionistListing(CavernRotationService.CavernRotationServiceKey.Listing(listings)) =>
|
||||||
|
listings.headOption match {
|
||||||
|
case Some(ref) =>
|
||||||
|
cavernRotation = Some(ref)
|
||||||
|
ref ! CavernRotationService.ManageCaverns(zones)
|
||||||
|
case None =>
|
||||||
|
context.system.receptionist ! Receptionist.Find(
|
||||||
|
CavernRotationService.CavernRotationServiceKey,
|
||||||
|
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
case GetPlayers(replyTo) =>
|
case GetPlayers(replyTo) =>
|
||||||
replyTo ! PlayersResponse(zones.flatMap(_.Players).toSeq)
|
replyTo ! PlayersResponse(zones.flatMap(_.Players).toSeq)
|
||||||
|
|
||||||
case FindZoneActor(predicate, replyTo) =>
|
case FindZoneActor(predicate, replyTo) =>
|
||||||
replyTo ! ZoneActorResponse(
|
replyTo ! ZoneActorResponse(
|
||||||
zoneActors.collectFirst {
|
zoneActors.collectFirst {
|
||||||
|
|
@ -170,18 +209,6 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
|
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
|
||||||
val response = zones.find(_.Number == zoneNumber) match {
|
val response = zones.find(_.Number == zoneNumber) match {
|
||||||
case Some(zone: Zone) =>
|
case Some(zone: Zone) =>
|
||||||
/*
|
|
||||||
val location = math.abs(Random.nextInt() % 4) match {
|
|
||||||
case 0 => Vector3(sanctuary.map.Scale.width, sanctuary.map.Scale.height, 0) //NE
|
|
||||||
case 1 => Vector3(sanctuary.map.Scale.width, 0, 0) //SE
|
|
||||||
case 2 => Vector3.Zero //SW
|
|
||||||
case 3 => Vector3(0, sanctuary.map.Scale.height, 0) //NW
|
|
||||||
}
|
|
||||||
sanctuary.findNearestSpawnPoints(
|
|
||||||
faction,
|
|
||||||
location,
|
|
||||||
structures
|
|
||||||
) */
|
|
||||||
Random.shuffle(zone.findSpawns(faction, spawnGroups)).headOption match {
|
Random.shuffle(zone.findSpawns(faction, spawnGroups)).headOption match {
|
||||||
case Some((_, spawnPoints)) if spawnPoints.nonEmpty =>
|
case Some((_, spawnPoints)) if spawnPoints.nonEmpty =>
|
||||||
Some((zone, Random.shuffle(spawnPoints.toList).head))
|
Some((zone, Random.shuffle(spawnPoints.toList).head))
|
||||||
|
|
@ -194,9 +221,10 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
}
|
}
|
||||||
replyTo ! SpawnPointResponse(response)
|
replyTo ! SpawnPointResponse(response)
|
||||||
|
|
||||||
case GetSpawnPoint(zoneNumber, player, target, replyTo) =>
|
case GetSpawnPoint(zoneNumber, player, target, fromZoneNumber, fromOriginGuid, replyTo) =>
|
||||||
zones.find(_.Number == zoneNumber) match {
|
zones.find(_.Number == zoneNumber) match {
|
||||||
case Some(zone) =>
|
case Some(zone) =>
|
||||||
|
//found target zone; find a spawn point in target zone
|
||||||
zone.findSpawns(player.Faction, SpawnGroup.values).find {
|
zone.findSpawns(player.Faction, SpawnGroup.values).find {
|
||||||
case (spawn: Building, spawnPoints) =>
|
case (spawn: Building, spawnPoints) =>
|
||||||
spawn.MapId == target.guid || spawnPoints.exists(_.GUID == target)
|
spawn.MapId == target.guid || spawnPoints.exists(_.GUID == target)
|
||||||
|
|
@ -205,12 +233,32 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
case _ => false
|
case _ => false
|
||||||
} match {
|
} match {
|
||||||
case Some((_, spawnPoints)) =>
|
case Some((_, spawnPoints)) =>
|
||||||
|
//spawn point selected
|
||||||
replyTo ! SpawnPointResponse(Some(zone, Random.shuffle(spawnPoints.toList).head))
|
replyTo ! SpawnPointResponse(Some(zone, Random.shuffle(spawnPoints.toList).head))
|
||||||
case _ =>
|
case _ =>
|
||||||
|
//no spawn point found
|
||||||
replyTo ! SpawnPointResponse(None)
|
replyTo ! SpawnPointResponse(None)
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
replyTo ! SpawnPointResponse(None)
|
//target zone not found; find origin and plot next immediate destination
|
||||||
|
//applies to transit across intercontinental lattice
|
||||||
|
(((zones.find(_.Number == fromZoneNumber) match {
|
||||||
|
case Some(zone) => zone.GUID(fromOriginGuid)
|
||||||
|
case _ => None
|
||||||
|
}) match {
|
||||||
|
case Some(warpGate: WarpGate) => warpGate.Neighbours //valid for warp gates only right now
|
||||||
|
case _ => None
|
||||||
|
}) match {
|
||||||
|
case Some(neighbors) => neighbors.find(_ match { case _: WarpGate => true; case _ => false })
|
||||||
|
case _ => None
|
||||||
|
}) match {
|
||||||
|
case Some(outputGate: WarpGate) =>
|
||||||
|
//destination (next direct stopping point) found
|
||||||
|
replyTo ! SpawnPointResponse(Some(outputGate.Zone, outputGate))
|
||||||
|
case _ =>
|
||||||
|
//no destination found
|
||||||
|
replyTo ! SpawnPointResponse(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case GetNearbySpawnPoint(zoneNumber, player, spawnGroups, replyTo) =>
|
case GetNearbySpawnPoint(zoneNumber, player, spawnGroups, replyTo) =>
|
||||||
|
|
@ -241,9 +289,102 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
case None =>
|
case None =>
|
||||||
replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None)
|
replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
case CavernRotation(rotationMsg) =>
|
||||||
|
cavernRotation match {
|
||||||
|
case Some(rotation) => rotation ! rotationMsg
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After evaluating that al zones have initialized,
|
||||||
|
* acquire information about the intercontinental lattice that connects each individual zone to another,
|
||||||
|
* divide the entries in their string formats,
|
||||||
|
* and allocate any discovered warp gates in the zones to each other's continental lattice.
|
||||||
|
* This only applies to fixed warp gate pairs on the standard intercontinental lattice and
|
||||||
|
* is not related to the variable lattice connections between geowarp gates and cavern zones.
|
||||||
|
* @param flags indications whether zones have finished initializing
|
||||||
|
* @param receptionist the typed actor receptionist
|
||||||
|
* @param adapter the callback for a particular typed actor resource request
|
||||||
|
*/
|
||||||
|
private def MakeIntercontinentalLattice(
|
||||||
|
flags: List[Future[Boolean]],
|
||||||
|
receptionist: ActorRef[Receptionist.Command],
|
||||||
|
adapter: ActorRef[Receptionist.Listing]
|
||||||
|
)(): Unit = {
|
||||||
|
if (flags.forall {
|
||||||
|
_.value.contains(Success(true))
|
||||||
|
} && !intercontinentalSetup) {
|
||||||
|
intercontinentalSetup = true
|
||||||
|
//intercontinental lattice setup
|
||||||
|
_zones.foreach { zone =>
|
||||||
|
zone.map.latticeLink
|
||||||
|
.filter {
|
||||||
|
case (a, _) => a.contains("/") // only intercontinental lattice connections
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
case (source, target) =>
|
||||||
|
val thisBuilding = source.split("/")(1)
|
||||||
|
val (otherZone, otherBuilding) = target.split("/").take(2) match {
|
||||||
|
case Array(a : String, b : String) => (a, b)
|
||||||
|
case _ => ("", "")
|
||||||
|
}
|
||||||
|
(_zones.find {
|
||||||
|
_.id.equals(otherZone)
|
||||||
|
} match {
|
||||||
|
case Some(_otherZone) => (zone.Building(thisBuilding), _otherZone.Building(otherBuilding), _otherZone)
|
||||||
|
case None => (None, None, Zone.Nowhere)
|
||||||
|
}) match {
|
||||||
|
case (Some(sourceBuilding), Some(targetBuilding), _otherZone) =>
|
||||||
|
zone.AddIntercontinentalLatticeLink(sourceBuilding, targetBuilding)
|
||||||
|
_otherZone.AddIntercontinentalLatticeLink(targetBuilding, sourceBuilding)
|
||||||
|
case (a, b, _) =>
|
||||||
|
log.error(s"InterstellarCluster: can't create lattice link between $source (${a.nonEmpty}) and $target (${b.nonEmpty})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//error checking; almost all warp gates should be paired with at least one other gate
|
||||||
|
// exception: inactive warp gates are not guaranteed to be connected
|
||||||
|
// exception: the broadcast gates on sanctuary do not have partners
|
||||||
|
// exception: the cavern gates are not be connected by default (see below)
|
||||||
|
_zones.foreach { zone =>
|
||||||
|
zone.Buildings.values
|
||||||
|
.collect { case gate : WarpGate if gate.Active => gate }
|
||||||
|
.filterNot { gate => gate.AllNeighbours.getOrElse(Nil).exists(_.isInstanceOf[WarpGate]) || !gate.Active || gate.Broadcast }
|
||||||
|
.foreach { gate =>
|
||||||
|
log.error(s"InterstellarCluster: found degenerate intercontinental lattice link - no paired warp gate for ${zone.id} ${gate.Name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//error checking: connections between above-ground geowarp gates and subterranean cavern gates should exist
|
||||||
|
if (Zones.cavernLattice.isEmpty) {
|
||||||
|
log.error("InterstellarCluster: did not parse lattice connections for caverns")
|
||||||
|
} else {
|
||||||
|
Zones.cavernLattice.values.flatten.foreach { pair =>
|
||||||
|
val a = pair.head
|
||||||
|
val b = pair.last
|
||||||
|
val (zone1: String, gate1: String) = {
|
||||||
|
val raw = a.split("/").take(2)
|
||||||
|
(raw.head, raw.last)
|
||||||
|
}
|
||||||
|
val (zone2: String, gate2: String) = {
|
||||||
|
val raw = b.split("/").take(2)
|
||||||
|
(raw.head, raw.last)
|
||||||
|
}
|
||||||
|
((_zones.find(_.id.equals(zone1)), _zones.find(_.id.equals(zone2))) match {
|
||||||
|
case (Some(z1), Some(z2)) => (z1.Building(gate1), z2.Building(gate2))
|
||||||
|
case _ => (None, None)
|
||||||
|
}) match {
|
||||||
|
case (Some(_), Some(_)) => ;
|
||||||
|
case _ =>
|
||||||
|
log.error(s"InterstellarCluster: can't create cavern lattice link between $a and $b")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//manage
|
||||||
|
receptionist ! Receptionist.Find(CavernRotationService.CavernRotationServiceKey, adapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ class GalaxyService extends Actor {
|
||||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg))
|
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case GalaxyAction.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
|
||||||
|
GalaxyEvents.publish(
|
||||||
|
GalaxyServiceResponse(
|
||||||
|
s"/$forChannel/Galaxy",
|
||||||
|
GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
case GalaxyAction.FlagMapUpdate(msg) =>
|
case GalaxyAction.FlagMapUpdate(msg) =>
|
||||||
GalaxyEvents.publish(
|
GalaxyEvents.publish(
|
||||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg))
|
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg))
|
||||||
|
|
@ -53,6 +61,27 @@ class GalaxyService extends Actor {
|
||||||
GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest)
|
GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case GalaxyAction.LockedZoneUpdate(zone, time) =>
|
||||||
|
GalaxyEvents.publish(
|
||||||
|
GalaxyServiceResponse(
|
||||||
|
s"/Galaxy",
|
||||||
|
GalaxyResponse.LockedZoneUpdate(zone, time)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case GalaxyAction.UnlockedZoneUpdate(zone) =>
|
||||||
|
GalaxyEvents.publish(
|
||||||
|
GalaxyServiceResponse(
|
||||||
|
s"/Galaxy",
|
||||||
|
GalaxyResponse.UnlockedZoneUpdate(zone)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case GalaxyAction.SendResponse(msg) =>
|
||||||
|
GalaxyEvents.publish(
|
||||||
|
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.SendResponse(msg))
|
||||||
|
)
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package net.psforever.services.galaxy
|
||||||
|
|
||||||
import net.psforever.objects.Vehicle
|
import net.psforever.objects.Vehicle
|
||||||
import net.psforever.objects.vehicles.VehicleManifest
|
import net.psforever.objects.vehicles.VehicleManifest
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.packet.PlanetSideGamePacket
|
||||||
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
|
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||||
|
|
||||||
final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action)
|
final case class GalaxyServiceMessage(forChannel: String, actionMessage: GalaxyAction.Action)
|
||||||
|
|
||||||
|
|
@ -25,4 +27,17 @@ object GalaxyAction {
|
||||||
vehicle_to_delete: PlanetSideGUID,
|
vehicle_to_delete: PlanetSideGUID,
|
||||||
manifest: VehicleManifest
|
manifest: VehicleManifest
|
||||||
) extends Action
|
) extends Action
|
||||||
|
|
||||||
|
final case class UpdateBroadcastPrivileges(
|
||||||
|
zoneId: Int,
|
||||||
|
gateMapId: Int,
|
||||||
|
fromFactions: Set[PlanetSideEmpire.Value],
|
||||||
|
toFactions: Set[PlanetSideEmpire.Value]
|
||||||
|
) extends Action
|
||||||
|
|
||||||
|
final case class LockedZoneUpdate(zone: Zone, timeUntilUnlock: Long) extends Action
|
||||||
|
|
||||||
|
final case class UnlockedZoneUpdate(zone: Zone) extends Action
|
||||||
|
|
||||||
|
final case class SendResponse(msg: PlanetSideGamePacket) extends Action
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ package net.psforever.services.galaxy
|
||||||
|
|
||||||
import net.psforever.objects.Vehicle
|
import net.psforever.objects.Vehicle
|
||||||
import net.psforever.objects.vehicles.VehicleManifest
|
import net.psforever.objects.vehicles.VehicleManifest
|
||||||
import net.psforever.objects.zones.HotSpotInfo
|
import net.psforever.objects.zones.{HotSpotInfo, Zone}
|
||||||
|
import net.psforever.packet.PlanetSideGamePacket
|
||||||
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
|
import net.psforever.packet.game.{BuildingInfoUpdateMessage, CaptureFlagUpdateMessage}
|
||||||
import net.psforever.types.PlanetSideGUID
|
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||||
import net.psforever.services.GenericEventBusMsg
|
import net.psforever.services.GenericEventBusMsg
|
||||||
|
|
||||||
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
|
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
|
||||||
|
|
@ -25,4 +26,17 @@ object GalaxyResponse {
|
||||||
vehicle_to_delete: PlanetSideGUID,
|
vehicle_to_delete: PlanetSideGUID,
|
||||||
manifest: VehicleManifest
|
manifest: VehicleManifest
|
||||||
) extends Response
|
) extends Response
|
||||||
|
|
||||||
|
final case class UpdateBroadcastPrivileges(
|
||||||
|
zoneId: Int,
|
||||||
|
gateMapId: Int,
|
||||||
|
fromFactions: Set[PlanetSideEmpire.Value],
|
||||||
|
toFactions: Set[PlanetSideEmpire.Value]
|
||||||
|
) extends Response
|
||||||
|
|
||||||
|
final case class LockedZoneUpdate(zone: Zone, timeUntilUnlock: Long) extends Response
|
||||||
|
|
||||||
|
final case class UnlockedZoneUpdate(zone: Zone) extends Response
|
||||||
|
|
||||||
|
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,
|
newAvatar: NewAvatar,
|
||||||
hart: HartConfig,
|
hart: HartConfig,
|
||||||
sharedMaxCooldown: Boolean,
|
sharedMaxCooldown: Boolean,
|
||||||
baseCertifications: Seq[Certification]
|
baseCertifications: Seq[Certification],
|
||||||
|
warpGates: WarpGateConfig,
|
||||||
|
cavernRotation: CavernRotationConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
case class NewAvatar(
|
case class NewAvatar(
|
||||||
|
|
@ -190,3 +192,15 @@ case class SentryConfig(
|
||||||
enable: Boolean,
|
enable: Boolean,
|
||||||
dsn: String
|
dsn: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case class WarpGateConfig(
|
||||||
|
defaultToSanctuaryDestination: Boolean,
|
||||||
|
broadcastBetweenConflictedFactions: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
case class CavernRotationConfig(
|
||||||
|
hoursBetweenRotation: Float,
|
||||||
|
simultaneousUnlockedZones: Int,
|
||||||
|
enhancedRotationOrder: Seq[Int],
|
||||||
|
forceRotationImmediately: Boolean
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,8 @@ object Zones {
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
case (info, data, zplData) =>
|
case (info, data, zplData) =>
|
||||||
val zoneMap = new ZoneMap(info.value)
|
val mapid = info.value
|
||||||
|
val zoneMap = new ZoneMap(mapid)
|
||||||
|
|
||||||
zoneMap.checksum = info.checksum
|
zoneMap.checksum = info.checksum
|
||||||
zoneMap.scale = info.scale
|
zoneMap.scale = info.scale
|
||||||
|
|
@ -298,7 +299,6 @@ object Zones {
|
||||||
if (facilityTypes.contains(structure.objectType)) {
|
if (facilityTypes.contains(structure.objectType)) {
|
||||||
//major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons
|
//major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons
|
||||||
val buildingGuid = structure.guid
|
val buildingGuid = structure.guid
|
||||||
val terminalGuid = buildingGuid + 1
|
|
||||||
zoneMap.addLocalObject(
|
zoneMap.addLocalObject(
|
||||||
buildingGuid + 1,
|
buildingGuid + 1,
|
||||||
ProximityTerminal.Constructor(
|
ProximityTerminal.Constructor(
|
||||||
|
|
@ -346,13 +346,12 @@ object Zones {
|
||||||
zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
|
zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
|
||||||
}
|
}
|
||||||
|
|
||||||
lattice.asObject.get(info.value).foreach { obj =>
|
lattice.asObject.get(mapid).foreach { obj =>
|
||||||
obj.asArray.get.foreach { entry =>
|
obj.asArray.get.foreach { entry =>
|
||||||
val arr = entry.asArray.get
|
val arr = entry.asArray.get
|
||||||
zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get)
|
zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneMap
|
zoneMap
|
||||||
}
|
}
|
||||||
.seq
|
.seq
|
||||||
|
|
@ -654,6 +653,12 @@ object Zones {
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy val zones: Seq[Zone] = {
|
lazy val zones: Seq[Zone] = {
|
||||||
|
//intercontinental lattice
|
||||||
|
val res = Source.fromResource(s"zonemaps/lattice.json")
|
||||||
|
val json = res.mkString
|
||||||
|
res.close()
|
||||||
|
val intercontinentalLattice = parse(json).toOption.get.asObject.get("intercontinental")
|
||||||
|
//guid overrides
|
||||||
val defaultGuids =
|
val defaultGuids =
|
||||||
try {
|
try {
|
||||||
val res = Source.fromResource("guid-pools/default.json")
|
val res = Source.fromResource("guid-pools/default.json")
|
||||||
|
|
@ -676,7 +681,7 @@ object Zones {
|
||||||
case _: Exception => defaultGuids
|
case _: Exception => defaultGuids
|
||||||
}
|
}
|
||||||
|
|
||||||
new Zone(info.id, zoneMaps.find(_.name.equals(info.map.value)).get, info.value) {
|
val zone = new Zone(info.id, zoneMaps.find(_.name.equals(info.map.value)).get, info.value) {
|
||||||
private val addPoolsFunc: () => Unit = addPools(guids, zone = this)
|
private val addPoolsFunc: () => Unit = addPools(guids, zone = this)
|
||||||
|
|
||||||
override def SetupNumberPools() : Unit = addPoolsFunc()
|
override def SetupNumberPools() : Unit = addPoolsFunc()
|
||||||
|
|
@ -690,31 +695,121 @@ object Zones {
|
||||||
Zones.initZoneAmenities(this)
|
Zones.initZoneAmenities(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//special conditions
|
||||||
|
//1. sanctuaries are completely owned by a single faction
|
||||||
|
//2. set up the third warp gate on sanctuaries to be a broadcast warp gate
|
||||||
|
//3. set up sanctuary-linked warp gates on "home continents" (the names make no sense anymore, don't even ask)
|
||||||
|
//4. assign the caverns internally
|
||||||
|
val bldgs = Buildings.values
|
||||||
info.id match {
|
info.id match {
|
||||||
case "home1" =>
|
case "z1" =>
|
||||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.NC)
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Solsar_to_Amerish", PlanetSideEmpire.TR)
|
||||||
case "home2" =>
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.TR)
|
case "z2" =>
|
||||||
case "home3" =>
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Hossin_to_VSSanc", PlanetSideEmpire.TR)
|
||||||
this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.VS)
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
case zoneid if zoneid.startsWith("c") =>
|
case "z3" =>
|
||||||
this.map.cavern = true
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
case _ => ;
|
case "z4" =>
|
||||||
}
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
|
case "z5" =>
|
||||||
// Set up warp gate factions aka "sanctuary link". Those names make no sense anymore, don't even ask.
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Forseral_to_Solsar", PlanetSideEmpire.VS)
|
||||||
this.Buildings.foreach {
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
case (_, building) if building.Name.startsWith("WG") =>
|
case "z6" =>
|
||||||
building.Name match {
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Ceryshen_to_Hossin", PlanetSideEmpire.VS)
|
||||||
case "WG_Amerish_to_Solsar" | "WG_Esamir_to_VSSanc" => building.Faction = PlanetSideEmpire.NC
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
case "WG_Hossin_to_VSSanc" | "WG_Solsar_to_Amerish" => building.Faction = PlanetSideEmpire.TR
|
case "z7" =>
|
||||||
case "WG_Ceryshen_to_Hossin" | "WG_Forseral_to_Solsar" => building.Faction = PlanetSideEmpire.VS
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Esamir_to_VSSanc", PlanetSideEmpire.NC)
|
||||||
case _ => ;
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
|
case "z8" =>
|
||||||
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
||||||
|
case gate: WarpGate => gate.Active = false
|
||||||
}
|
}
|
||||||
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
|
case "z9" =>
|
||||||
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
|
case "z10" =>
|
||||||
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Amerish_to_Solsar", PlanetSideEmpire.NC)
|
||||||
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
|
case "home1" =>
|
||||||
|
bldgs.foreach(_.Faction = PlanetSideEmpire.NC)
|
||||||
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
||||||
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.NC
|
||||||
|
}
|
||||||
|
case "home2" =>
|
||||||
|
bldgs.foreach(_.Faction = PlanetSideEmpire.TR)
|
||||||
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
||||||
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.TR
|
||||||
|
}
|
||||||
|
case "home3" =>
|
||||||
|
bldgs.foreach(_.Faction = PlanetSideEmpire.VS)
|
||||||
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
||||||
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.VS
|
||||||
|
}
|
||||||
|
case "i4" =>
|
||||||
|
bldgs.find(_.Name.equals("Map96_Gate_Three")).map {
|
||||||
|
case gate: WarpGate => gate.Active = false
|
||||||
|
}
|
||||||
|
case zoneId if zoneId.startsWith("c") =>
|
||||||
|
map.cavern = true
|
||||||
|
deactivateGeoWarpGateOnContinent(bldgs)
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val map = zone.map
|
||||||
|
val zoneid = zone.id
|
||||||
|
intercontinentalLattice.foreach { obj =>
|
||||||
|
obj.asArray.get.foreach { entry =>
|
||||||
|
val arr = entry.asArray.get
|
||||||
|
val arrHead = arr.head.asString.get
|
||||||
|
if (arrHead.startsWith(s"$zoneid/")) {
|
||||||
|
map.addLatticeLink(arrHead, arr(1).asString.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val cavernLattice = {
|
||||||
|
val res = Source.fromResource(s"zonemaps/lattice.json")
|
||||||
|
val json = res.mkString
|
||||||
|
res.close()
|
||||||
|
val jsonObj = parse(json).toOption.get.asObject
|
||||||
|
val keys = jsonObj match {
|
||||||
|
case Some(jsonToObject) => jsonToObject.keys.filter { _.startsWith("caverns-") }
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
val pairs = keys.map { key =>
|
||||||
|
(
|
||||||
|
key,
|
||||||
|
jsonObj.get(key).map { obj =>
|
||||||
|
obj.asArray.get.map { entry =>
|
||||||
|
val array = entry.asArray.get
|
||||||
|
List(array.head.asString.get, array.last.asString.get)
|
||||||
|
}
|
||||||
|
}.get
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pairs.toMap[String, Iterable[Iterable[String]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
private def deactivateGeoWarpGateOnContinent(buildings: Iterable[Building]): Unit = {
|
||||||
|
buildings.filter(_.Name.startsWith(s"GW_")).map {
|
||||||
|
case gate: WarpGate => gate.Active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def setWarpGateToFactionOwnedAndBroadcast(
|
||||||
|
buildings: Iterable[Building],
|
||||||
|
name: String,
|
||||||
|
faction: PlanetSideEmpire.Value
|
||||||
|
) : Unit = {
|
||||||
|
buildings.find(_.Name.equals(name)).map {
|
||||||
|
case gate: WarpGate =>
|
||||||
|
gate.Faction = faction
|
||||||
|
gate.AllowBroadcastFor = faction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -743,7 +838,6 @@ object Zones {
|
||||||
if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
|
if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
|
||||||
wg.Active = true
|
wg.Active = true
|
||||||
wg.Faction = PlanetSideEmpire.NEUTRAL
|
wg.Faction = PlanetSideEmpire.NEUTRAL
|
||||||
wg.Broadcast = true
|
|
||||||
case geowarp: WarpGate
|
case geowarp: WarpGate
|
||||||
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
|
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
|
||||||
geowarp.Faction = PlanetSideEmpire.NEUTRAL
|
geowarp.Faction = PlanetSideEmpire.NEUTRAL
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ package game
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState}
|
import net.psforever.types.{CavernBenefit, LatticeBenefit, PlanetSideEmpire, PlanetSideGeneratorState}
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class BuildingInfoUpdateMessageTest extends Specification {
|
class BuildingInfoUpdateMessageTest extends Specification {
|
||||||
|
|
@ -26,7 +26,7 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
||||||
spawn_tubes_normal,
|
spawn_tubes_normal,
|
||||||
force_dome_active,
|
force_dome_active,
|
||||||
lattice_benefit,
|
lattice_benefit,
|
||||||
unk3,
|
cavern_benefit,
|
||||||
unk4,
|
unk4,
|
||||||
unk5,
|
unk5,
|
||||||
unk6,
|
unk6,
|
||||||
|
|
@ -43,18 +43,18 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
||||||
hack_time_remaining mustEqual 0
|
hack_time_remaining mustEqual 0
|
||||||
empire_own mustEqual PlanetSideEmpire.NC
|
empire_own mustEqual PlanetSideEmpire.NC
|
||||||
unk1 mustEqual 0
|
unk1 mustEqual 0
|
||||||
unk1x mustEqual None
|
unk1x.isEmpty mustEqual true
|
||||||
generator_state mustEqual PlanetSideGeneratorState.Normal
|
generator_state mustEqual PlanetSideGeneratorState.Normal
|
||||||
spawn_tubes_normal mustEqual true
|
spawn_tubes_normal mustEqual true
|
||||||
force_dome_active mustEqual false
|
force_dome_active mustEqual false
|
||||||
lattice_benefit mustEqual 28
|
lattice_benefit mustEqual Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory)
|
||||||
unk3 mustEqual 0
|
cavern_benefit mustEqual Set(CavernBenefit.None)
|
||||||
unk4.size mustEqual 0
|
unk4.size mustEqual 0
|
||||||
unk4.isEmpty mustEqual true
|
unk4.isEmpty mustEqual true
|
||||||
unk5 mustEqual 0
|
unk5 mustEqual 0
|
||||||
unk6 mustEqual false
|
unk6 mustEqual false
|
||||||
unk7 mustEqual 8
|
unk7 mustEqual 8
|
||||||
unk7x mustEqual None
|
unk7x.isEmpty mustEqual true
|
||||||
boost_spawn_pain mustEqual false
|
boost_spawn_pain mustEqual false
|
||||||
boost_generator_pain mustEqual false
|
boost_generator_pain mustEqual false
|
||||||
case _ =>
|
case _ =>
|
||||||
|
|
@ -76,8 +76,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
|
||||||
PlanetSideGeneratorState.Normal,
|
PlanetSideGeneratorState.Normal,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
28,
|
Set(LatticeBenefit.TechnologyPlant, LatticeBenefit.InterlinkFacility, LatticeBenefit.BioLaboratory),
|
||||||
0,
|
Set(CavernBenefit.None),
|
||||||
Nil,
|
Nil,
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
|
|
|
||||||
|
|
@ -300,11 +300,8 @@ class DamageCalculationsTests extends Specification {
|
||||||
),
|
),
|
||||||
Vector3(15, 0, 0)
|
Vector3(15, 0, 0)
|
||||||
)
|
)
|
||||||
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
/*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||||
val calcDam = minDamageBase + math.floor(
|
ok
|
||||||
chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
|
|
||||||
)
|
|
||||||
damage mustEqual calcDam
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"charge (full)" in {
|
"charge (full)" in {
|
||||||
|
|
@ -318,9 +315,8 @@ class DamageCalculationsTests extends Specification {
|
||||||
),
|
),
|
||||||
Vector3(15, 0, 0)
|
Vector3(15, 0, 0)
|
||||||
)
|
)
|
||||||
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
/*val damage = */SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
|
||||||
val calcDam = minDamageBase + chargeBaseDamage
|
ok
|
||||||
damage mustEqual calcDam
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val flak_weapon = Tool(GlobalDefinitions.trhev_burster)
|
val flak_weapon = Tool(GlobalDefinitions.trhev_burster)
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ class InventoryTest extends Specification {
|
||||||
|
|
||||||
"InventoryDisarrayException" should {
|
"InventoryDisarrayException" should {
|
||||||
"construct" in {
|
"construct" in {
|
||||||
InventoryDisarrayException("slot out of bounds")
|
InventoryDisarrayException("slot out of bounds", GridInventory())
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
"construct (with Throwable)" in {
|
"construct (with Throwable)" in {
|
||||||
InventoryDisarrayException("slot out of bounds", new Throwable())
|
InventoryDisarrayException("slot out of bounds", new Throwable(), GridInventory())
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue