mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
* players will properly be blamed for being killed in vehicles * vehicle occupants should stay in their seats until told to die * wall turrets are now recognized as something else
1011 lines
38 KiB
Scala
1011 lines
38 KiB
Scala
package net.psforever.zones
|
|
|
|
import java.io.FileNotFoundException
|
|
import net.psforever.objects.serverobject.terminals.{ProximityTerminal, ProximityTerminalDefinition, Terminal, TerminalDefinition}
|
|
import net.psforever.objects.serverobject.mblocker.Locker
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger
|
|
import akka.actor.ActorContext
|
|
import io.circe._
|
|
import io.circe.parser._
|
|
import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile}
|
|
import net.psforever.objects.definition.BasicDefinition
|
|
import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector}
|
|
import net.psforever.objects.serverobject.doors.{Door, DoorDefinition, SpawnTubeDoor}
|
|
import net.psforever.objects.serverobject.generator.Generator
|
|
import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition}
|
|
import net.psforever.objects.serverobject.locks.IFFLock
|
|
import net.psforever.objects.serverobject.pad.{VehicleSpawnPad, VehicleSpawnPadDefinition}
|
|
import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition}
|
|
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
|
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
|
|
import net.psforever.objects.serverobject.structures.{Building, BuildingDefinition, FoundationBuilder, StructureType, WarpGate}
|
|
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalDefinition}
|
|
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
|
import net.psforever.objects.serverobject.tube.SpawnTube
|
|
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
|
|
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
|
import net.psforever.objects.sourcing.{DeployableSource, ObjectSource, PlayerSource, TurretSource, VehicleSource}
|
|
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
|
|
import net.psforever.types.{Angular, PlanetSideEmpire, Vector3}
|
|
import net.psforever.util.DefinitionUtil
|
|
|
|
import scala.io.Source
|
|
import scala.collection.parallel.CollectionConverters._
|
|
|
|
object Zones {
|
|
|
|
private case class ZoneMapEntity(
|
|
id: Int,
|
|
objectName: String,
|
|
objectType: String,
|
|
owner: Option[Int],
|
|
absX: Float,
|
|
absY: Float,
|
|
absZ: Float,
|
|
yaw: Float,
|
|
guid: Int,
|
|
mapId: Option[Int],
|
|
isChildObject: Boolean
|
|
) {
|
|
val position: Vector3 = Vector3(absX, absY, absZ)
|
|
|
|
def objectDefinition: BasicDefinition = {
|
|
DefinitionUtil.fromString(objectType)
|
|
}
|
|
}
|
|
|
|
private case class GuidNumberPool(
|
|
name: String,
|
|
start: Int,
|
|
max: Int,
|
|
selector: String
|
|
) {
|
|
def getSelector(): NumberSelector = {
|
|
if (selector.equals("random")) new RandomSelector
|
|
else new SpecificSelector
|
|
}
|
|
}
|
|
|
|
private implicit val decodeNumberPool: Decoder[GuidNumberPool] = Decoder.forProduct4(
|
|
"Name",
|
|
"Start",
|
|
"Max",
|
|
"Selector"
|
|
)(GuidNumberPool.apply)
|
|
|
|
private implicit val decodeZoneMapEntity: Decoder[ZoneMapEntity] = Decoder.forProduct11(
|
|
"Id",
|
|
"ObjectName",
|
|
"ObjectType",
|
|
"Owner",
|
|
"AbsX",
|
|
"AbsY",
|
|
"AbsZ",
|
|
"Yaw",
|
|
"GUID",
|
|
"MapID",
|
|
"IsChildObject"
|
|
)(ZoneMapEntity.apply)
|
|
|
|
private implicit val decodeVector3 : Decoder[Vector3] = Decoder.forProduct3(
|
|
"X",
|
|
"Y",
|
|
"Z"
|
|
)(Vector3.apply)
|
|
|
|
private implicit val decodeZipLinePath: Decoder[ZipLinePath] = Decoder.forProduct3(
|
|
"PathId",
|
|
"IsTeleporter",
|
|
"PathPoints"
|
|
)(ZipLinePath.apply)
|
|
|
|
// monolith, hst, warpgate are ignored for now as the scala code isn't ready to handle them.
|
|
// BFR terminals/doors are ignored as top level elements as sanctuaries have them with no associated building. (repair_silo also has this problem, but currently is ignored in the AmenityExtrator project)
|
|
// Force domes have GUIDs but are currently classed as separate entities. The dome is controlled by sending GOAM 44 / 48 / 52 to the building GUID
|
|
private val ignoredEntities = Seq(
|
|
"monolith",
|
|
"force_dome_dsp_physics",
|
|
"force_dome_comm_physics",
|
|
"force_dome_cryo_physics",
|
|
"force_dome_tech_physics",
|
|
"force_dome_amp_physics"
|
|
)
|
|
|
|
private val towerTypes = Seq("tower_a", "tower_b", "tower_c")
|
|
private val facilityTypes = Seq("amp_station", "cryo_facility", "comm_station", "comm_station_dsp", "tech_plant")
|
|
private val bunkerTypes = Seq("bunker_gauntlet", "bunker_lg", "bunker_sm")
|
|
private val warpGateTypes = Seq("hst", "warpgate", "warpgate_small", "warpgate_cavern")
|
|
private val miscBuildingTypes = Seq(
|
|
"orbital_building_vs",
|
|
"orbital_building_tr",
|
|
"orbital_building_nc",
|
|
"VT_building_vs",
|
|
"VT_building_tr",
|
|
"VT_building_nc",
|
|
"vt_dropship",
|
|
"vt_spawn",
|
|
"vt_vehicle"
|
|
)
|
|
private val cavernBuildingTypes = Seq(
|
|
"ceiling_bldg_a",
|
|
"ceiling_bldg_b",
|
|
"ceiling_bldg_c",
|
|
"ceiling_bldg_d",
|
|
"ceiling_bldg_e",
|
|
"ceiling_bldg_f",
|
|
"ceiling_bldg_g",
|
|
"ceiling_bldg_h",
|
|
"ceiling_bldg_i",
|
|
"ceiling_bldg_j",
|
|
"ceiling_bldg_z",
|
|
"ground_bldg_a",
|
|
"ground_bldg_b",
|
|
"ground_bldg_c",
|
|
"ground_bldg_d",
|
|
"ground_bldg_e",
|
|
"ground_bldg_f",
|
|
"ground_bldg_g",
|
|
"ground_bldg_h",
|
|
"ground_bldg_i",
|
|
"ground_bldg_j",
|
|
"ground_bldg_z",
|
|
"redoubt",
|
|
"vanu_control_point",
|
|
"vanu_core",
|
|
"vanu_vehicle_station"
|
|
)
|
|
private val basicTerminalTypes =
|
|
Seq("order_terminal", "spawn_terminal", "cert_terminal", "order_terminal", "vanu_equipment_term")
|
|
private val spawnPadTerminalTypes = Seq(
|
|
"ground_vehicle_terminal",
|
|
"air_vehicle_terminal",
|
|
"vehicle_terminal",
|
|
"vehicle_terminal_combined",
|
|
"dropship_vehicle_terminal",
|
|
"vanu_air_vehicle_term",
|
|
"vanu_vehicle_term",
|
|
"bfr_terminal"
|
|
)
|
|
private val terminalTypes = basicTerminalTypes ++ spawnPadTerminalTypes
|
|
|
|
private val spawnPadTypes = Seq(
|
|
"mb_pad_creation",
|
|
"dropship_pad_doors",
|
|
"vanu_vehicle_creation_pad",
|
|
"bfr_door"
|
|
)
|
|
|
|
private val doorTypes = Seq(
|
|
"gr_door_garage_int",
|
|
"gr_door_int",
|
|
"gr_door_med",
|
|
"spawn_tube_door",
|
|
"amp_cap_door",
|
|
"door_dsp",
|
|
"gr_door_ext",
|
|
"gr_door_garage_ext",
|
|
"gr_door_main",
|
|
"gr_door_mb_ext",
|
|
"gr_door_mb_int",
|
|
"gr_door_mb_lrg",
|
|
"gr_door_mb_obsd",
|
|
"gr_door_mb_orb",
|
|
"door_spawn_mb",
|
|
"ancient_door",
|
|
"ancient_garage_door"
|
|
)
|
|
|
|
lazy val zoneMaps: Seq[ZoneMap] = {
|
|
val res = Source.fromResource(s"zonemaps/lattice.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
val lattice = parse(json).toOption.get
|
|
|
|
MapInfo.values.par
|
|
.map { info =>
|
|
val data =
|
|
try {
|
|
val res = Source.fromResource(s"zonemaps/${info.value}.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
decode[Seq[ZoneMapEntity]](json).toOption.get.filter(e => !ignoredEntities.contains(e.objectType))
|
|
} catch {
|
|
case _: FileNotFoundException => Seq()
|
|
}
|
|
|
|
val zplData =
|
|
try {
|
|
val res = Source.fromResource(s"zonemaps/zpl_${info.value}.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
decode[Seq[ZipLinePath]](json).toOption.get
|
|
} catch {
|
|
case _: FileNotFoundException => Seq()
|
|
}
|
|
(info, data, zplData)
|
|
}
|
|
.map {
|
|
case (info, data, zplData) =>
|
|
val mapid = info.value
|
|
val zoneMap = new ZoneMap(mapid)
|
|
|
|
zoneMap.checksum = info.checksum
|
|
zoneMap.scale = info.scale
|
|
zoneMap.environment = info.environment
|
|
|
|
zoneMap.zipLinePaths = zplData.toList
|
|
|
|
// This keeps track of the last used turret weapon guid, as they seem to be arbitrarily assigned at 5000+
|
|
val turretWeaponGuid = new AtomicInteger(5000)
|
|
|
|
val (structures, zoneObjects) = data
|
|
.filter(!_.isChildObject)
|
|
.partition(e =>
|
|
facilityTypes.contains(e.objectType) ||
|
|
towerTypes.contains(e.objectType) ||
|
|
bunkerTypes.contains(e.objectType) ||
|
|
warpGateTypes.contains(e.objectType) ||
|
|
miscBuildingTypes.contains(e.objectType) ||
|
|
cavernBuildingTypes.contains(e.objectType)
|
|
)
|
|
|
|
structures.foreach { structure =>
|
|
// For some reason when spawning at a Redoubt building the client requests a spawn type of Tower
|
|
// likely to allow the choice of spawning at both Redoubt and Module buildings
|
|
val structureType =
|
|
if (towerTypes.contains(structure.objectType) || structure.objectType == "redoubt")
|
|
StructureType.Tower
|
|
else if (facilityTypes.contains(structure.objectType) || cavernBuildingTypes.contains(structure.objectType))
|
|
StructureType.Facility
|
|
else if (bunkerTypes.contains(structure.objectType))
|
|
StructureType.Bunker
|
|
else
|
|
StructureType.Building
|
|
// todo: Platform types
|
|
|
|
structure.objectType match {
|
|
case objectType @ "hst" if warpGateTypes.contains(objectType) =>
|
|
zoneMap.addLocalBuilding(
|
|
structure.objectName,
|
|
structure.guid,
|
|
structure.mapId.get,
|
|
FoundationBuilder(
|
|
WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.hst)
|
|
)
|
|
)
|
|
case objectType if warpGateTypes.contains(objectType) =>
|
|
zoneMap.addLocalBuilding(
|
|
structure.objectName,
|
|
structure.guid,
|
|
structure.mapId.get,
|
|
FoundationBuilder(WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ)))
|
|
)
|
|
case _ =>
|
|
zoneMap.addLocalBuilding(
|
|
structure.objectName,
|
|
structure.guid,
|
|
structure.mapId.get,
|
|
FoundationBuilder(
|
|
Building.Structure(
|
|
structureType,
|
|
Vector3(structure.absX, structure.absY, structure.absZ),
|
|
Vector3(0f, 0f, structure.yaw),
|
|
structure.objectDefinition.asInstanceOf[BuildingDefinition]
|
|
)
|
|
)
|
|
)
|
|
if (facilityTypes.contains(structure.objectType)) {
|
|
//major overworld facilities have an intrinsic terminal that occasionally recharges ancient weapons
|
|
val buildingGuid = structure.guid
|
|
zoneMap.addLocalObject(
|
|
buildingGuid + 1,
|
|
ProximityTerminal.Constructor(
|
|
structure.position,
|
|
GlobalDefinitions.recharge_terminal_weapon_module
|
|
),
|
|
owningBuildingGuid = buildingGuid
|
|
)
|
|
}
|
|
}
|
|
val filteredZoneEntities =
|
|
data.filter { _.owner.contains(structure.id) } ++
|
|
{
|
|
if (structure.objectType.startsWith("orbital_building_")) {
|
|
val structurePosition = structure.position
|
|
data.filter { entity =>
|
|
entity.objectType.startsWith("bfr_") &&
|
|
Vector3.DistanceSquared(entity.position, structurePosition) < 160000f
|
|
}
|
|
} else {
|
|
List()
|
|
}
|
|
}
|
|
createObjects(
|
|
zoneMap,
|
|
filteredZoneEntities,
|
|
structure.guid,
|
|
Some(structure),
|
|
turretWeaponGuid
|
|
)
|
|
}
|
|
|
|
createObjects(
|
|
zoneMap,
|
|
zoneObjects.filterNot { _.objectType.startsWith("bfr_") },
|
|
ownerGuid = 0,
|
|
None,
|
|
turretWeaponGuid
|
|
)
|
|
|
|
lattice.asObject.get(mapid).foreach { obj =>
|
|
obj.asArray.get.foreach { entry =>
|
|
val arr = entry.asArray.get
|
|
zoneMap.addLatticeLink(arr(0).asString.get, arr(1).asString.get)
|
|
}
|
|
}
|
|
zoneMap
|
|
}
|
|
.seq
|
|
}
|
|
|
|
private def createObjects(
|
|
zoneMap: ZoneMap,
|
|
objects: Seq[ZoneMapEntity],
|
|
ownerGuid: Int,
|
|
structure: Option[ZoneMapEntity],
|
|
turretWeaponGuid: AtomicInteger
|
|
): Unit = {
|
|
val spawnPads = objects.filter(e => spawnPadTypes.contains(e.objectType))
|
|
val doors = objects.filter(e => doorTypes.contains(e.objectType))
|
|
val implantTerminals = objects.filter(_.objectType == "implant_terminal")
|
|
val genControls = objects.filter(_.objectType == "gen_control")
|
|
|
|
objects.foreach { obj =>
|
|
if (ownerGuid == 0) assert(obj.owner.isEmpty)
|
|
|
|
obj.objectType match {
|
|
case "capture_terminal" | "secondary_capture" | "vanu_control_console" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
CaptureTerminal.Constructor(
|
|
obj.position,
|
|
obj.objectDefinition.asInstanceOf[CaptureTerminalDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
case "llm_socket" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
CaptureFlagSocket.Constructor(
|
|
obj.objectDefinition.asInstanceOf[CaptureFlagSocketDefinition],
|
|
obj.position
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case "gr_door_mb_orb" =>
|
|
zoneMap
|
|
.addLocalObject(
|
|
obj.guid,
|
|
Door.Constructor(obj.position, GlobalDefinitions.gr_door_mb_orb),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case doorType if doorType.equals("door_spawn_mb") || doorType.equals("spawn_tube_door") =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
SpawnTubeDoor.Constructor(
|
|
obj.position,
|
|
DefinitionUtil.fromString(doorType).asInstanceOf[DoorDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case objectType if doorTypes.contains(objectType) =>
|
|
zoneMap
|
|
.addLocalObject(obj.guid, Door.Constructor(obj.position), owningBuildingGuid = ownerGuid)
|
|
|
|
case "locker_cryo" | "locker_med" | "mb_locker" =>
|
|
zoneMap
|
|
.addLocalObject(obj.guid, Locker.Constructor(obj.position), owningBuildingGuid = ownerGuid)
|
|
|
|
case "lock_external" | "lock_garage" | "lock_small" =>
|
|
val closestDoor = doors.minBy(d => Vector3.Distance(d.position, obj.position))
|
|
|
|
// Since tech plant garage locks are the only type where the lock does not face the same direction as the door we need to apply an offset for those, otherwise the door won't operate properly when checking inside/outside angles.
|
|
val yawOffset = if (obj.objectType == "lock_garage") 90 else 0
|
|
|
|
// Ignore duplicate lock objects, for example Sobek (and other Dropship Centers) CC door has 2 locks stacked on top of each other
|
|
if (!zoneMap.doorToLock.keys.iterator.contains(closestDoor.guid)) {
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
IFFLock.Constructor(obj.position, Vector3(0, 0, obj.yaw + yawOffset)),
|
|
owningBuildingGuid = ownerGuid,
|
|
doorGuid = closestDoor.guid
|
|
)
|
|
}
|
|
|
|
case objectType if structure.isDefined && terminalTypes.contains(objectType) =>
|
|
// SoE in their infinite wisdom decided to remap vehicle_terminal to vehicle_terminal_combined in certain cases in the game_objects.adb file.
|
|
// As such, we have to work around it.
|
|
|
|
/*
|
|
startup.pak-out/game_objects.adb.lst:1097:add_property amp_station child_remap vehicle_terminal vehicle_terminal_combined
|
|
startup.pak-out/game_objects.adb.lst:7654:add_property comm_station child_remap vehicle_terminal vehicle_terminal_combined
|
|
startup.pak-out/game_objects.adb.lst:7807:add_property cryo_facility child_remap vehicle_terminal vehicle_terminal_combined
|
|
*/
|
|
val terminalType = (obj.objectType, structure.get.objectType) match {
|
|
case ("vehicle_terminal", "amp_station" | "comm_station" | "cryo_facility") =>
|
|
"vehicle_terminal_combined"
|
|
// FIXME we're always using ground_vehicle_terminal in place of vehicle_terminal
|
|
case ("vehicle_terminal", _) =>
|
|
"ground_vehicle_terminal"
|
|
case _ =>
|
|
obj.objectType
|
|
}
|
|
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
Terminal.Constructor(
|
|
obj.position,
|
|
DefinitionUtil.fromString(terminalType).asInstanceOf[TerminalDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
if (spawnPadTerminalTypes.contains(obj.objectType)) {
|
|
val closestSpawnPad =
|
|
spawnPads.minBy(point => Vector3.DistanceSquared(point.position, obj.position))
|
|
|
|
val adjustedYaw = structure match {
|
|
case Some(building)
|
|
if objectType.equals("bfr_terminal") =>
|
|
//bfr_terminal entities are paired with bfr_door entities
|
|
//rotations are not correctly set in the zone list, but assumptions can be made based on facility type
|
|
if (building.objectType.startsWith("orbital_building")) {
|
|
//sanctuary bfr sheds actually have their rotation angle set correctly in the zone map
|
|
obj.yaw + 45f
|
|
} else {
|
|
//predictable angles based on the facility type
|
|
Angular.flipClockwise(building.yaw) + (if (building.objectType.startsWith("comm_station")) { //includes comm_station_dsp
|
|
-45f
|
|
} else if (building.objectType.equals("cryo_facility") || building.objectType.equals("tech_plant")) {
|
|
135f
|
|
} else if (building.objectType.equals("amp_station")) {
|
|
225f
|
|
} else {
|
|
0f
|
|
})
|
|
}
|
|
case _ =>
|
|
//spawn pads have a default rotation that it +90 degrees from where it should be
|
|
//presumably the model is rotated differently to the expected orientation
|
|
closestSpawnPad.yaw - 90
|
|
}
|
|
|
|
zoneMap.addLocalObject(
|
|
closestSpawnPad.guid,
|
|
VehicleSpawnPad.Constructor(
|
|
closestSpawnPad.position,
|
|
closestSpawnPad.objectDefinition.asInstanceOf[VehicleSpawnPadDefinition],
|
|
Vector3(0, 0, adjustedYaw)
|
|
),
|
|
owningBuildingGuid = ownerGuid,
|
|
terminalGuid = obj.guid
|
|
)
|
|
}
|
|
|
|
case "resource_silo" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
ResourceSilo.Constructor(obj.position),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case "respawn_tube" | "mb_respawn_tube" | "redoubt_floor" | "vanu_spawn_room_pad" if structure.isDefined =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
if (towerTypes.contains(structure.get.objectType))
|
|
SpawnTube
|
|
.Constructor(obj.position, GlobalDefinitions.respawn_tube_tower, Vector3(0, 0, obj.yaw))
|
|
else if (structure.get.objectType.startsWith("VT_building_")) {
|
|
SpawnTube
|
|
.Constructor(obj.position, GlobalDefinitions.respawn_tube_sanctuary, Vector3(0, 0, obj.yaw))
|
|
} else
|
|
SpawnTube
|
|
.Constructor(obj.position, Vector3(0, 0, obj.yaw)),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case "adv_med_terminal" | "repair_silo" | "pad_landing_frame" | "pad_landing_tower_frame" | "medical_terminal" |
|
|
"crystals_health_a" | "crystals_health_b" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
ProximityTerminal
|
|
.Constructor(
|
|
obj.position,
|
|
obj.objectDefinition.asInstanceOf[ProximityTerminalDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
// Some objects such as repair_silo and pad_landing_frame have special terminal objects
|
|
// (e.g. bfr rearm, ground vehicle repair, ground vehicle rearm) that should follow immediately after,
|
|
// with incrementing GUIDs. As such, these will be hardcoded for now.
|
|
|
|
obj.objectType match {
|
|
case "repair_silo" =>
|
|
// startup.pak-out/game_objects.adb.lst:27235:add_property repair_silo has_aggregate_bfr_terminal true
|
|
// startup.pak-out/game_objects.adb.lst:27236:add_property repair_silo has_aggregate_rearm_terminal true
|
|
// startup.pak-out/game_objects.adb.lst:27237:add_property repair_silo has_aggregate_recharge_terminal true
|
|
zoneMap.addLocalObject(
|
|
obj.guid + 1,
|
|
Terminal.Constructor(obj.position, GlobalDefinitions.ground_rearm_terminal),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
zoneMap.addLocalObject(
|
|
obj.guid + 2,
|
|
Terminal.Constructor(obj.position, GlobalDefinitions.bfr_rearm_terminal),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
zoneMap.addLocalObject(
|
|
obj.guid + 3,
|
|
ProximityTerminal.Constructor(obj.position, GlobalDefinitions.recharge_terminal),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
case "pad_landing_frame" | "pad_landing_tower_frame" =>
|
|
// startup.pak-out/game_objects.adb.lst:22518:add_property pad_landing_frame has_aggregate_rearm_terminal true
|
|
// startup.pak-out/game_objects.adb.lst:22519:add_property pad_landing_frame has_aggregate_recharge_terminal true
|
|
// startup.pak-out/game_objects.adb.lst:22534:add_property pad_landing_tower_frame has_aggregate_rearm_terminal true
|
|
// startup.pak-out/game_objects.adb.lst:22535:add_property pad_landing_tower_frame has_aggregate_recharge_terminal true
|
|
zoneMap.addLocalObject(
|
|
obj.guid + 1,
|
|
Terminal.Constructor(obj.position, GlobalDefinitions.air_rearm_terminal),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
zoneMap.addLocalObject(
|
|
obj.guid + 2,
|
|
ProximityTerminal.Constructor(obj.position, GlobalDefinitions.recharge_terminal),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
case _ => ;
|
|
}
|
|
|
|
case "manned_turret" | "vanu_sentry_turret" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
FacilityTurret.Constructor(
|
|
obj.position,
|
|
obj.objectDefinition.asInstanceOf[FacilityTurretDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement())
|
|
|
|
case "implant_terminal_mech" =>
|
|
zoneMap.addLocalObject(
|
|
obj.guid,
|
|
ImplantTerminalMech.Constructor(obj.position),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
val closestTerminal = implantTerminals.minBy(e => Vector3.DistanceSquared(e.position, obj.position))
|
|
|
|
zoneMap.addLocalObject(
|
|
closestTerminal.guid,
|
|
Terminal.Constructor(closestTerminal.position, GlobalDefinitions.implant_terminal_interface),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
zoneMap.linkTerminalToInterface(obj.guid, closestTerminal.guid)
|
|
|
|
case "painbox" | "painbox_continuous" | "painbox_door_radius" | "painbox_door_radius_continuous" |
|
|
"painbox_radius" | "painbox_radius_continuous" =>
|
|
zoneMap
|
|
.addLocalObject(
|
|
obj.guid,
|
|
Painbox.Constructor(
|
|
obj.position,
|
|
obj.objectDefinition.asInstanceOf[PainboxDefinition]
|
|
),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case "generator" =>
|
|
zoneMap
|
|
.addLocalObject(
|
|
obj.guid,
|
|
Generator.Constructor(obj.position),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
val genControl = genControls.minBy(e => Vector3.DistanceSquared(e.position, obj.position))
|
|
|
|
zoneMap
|
|
.addLocalObject(
|
|
genControl.guid,
|
|
Terminal.Constructor(genControl.position, GlobalDefinitions.gen_control),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
|
|
case "obbasemesh" =>
|
|
zoneMap
|
|
.addLocalObject(
|
|
obj.guid,
|
|
OrbitalShuttlePad.Constructor(obj.position, GlobalDefinitions.obbasemesh, Vector3.z(obj.yaw)),
|
|
owningBuildingGuid = ownerGuid
|
|
)
|
|
zoneMap.linkShuttleToBay(obj.guid)
|
|
|
|
case _ => ()
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
lazy val zones: Seq[Zone] = {
|
|
//intercontinental lattice
|
|
val res = Source.fromResource(s"zonemaps/lattice.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
val intercontinentalLattice = parse(json).toOption.get.asObject.get("intercontinental")
|
|
//guid overrides
|
|
val defaultGuids =
|
|
try {
|
|
val res = Source.fromResource("guid-pools/default.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
decode[Seq[GuidNumberPool]](json).toOption.get
|
|
} catch {
|
|
case _: Exception => Seq()
|
|
}
|
|
|
|
ZoneInfo.values.map { info =>
|
|
val guids =
|
|
try {
|
|
val res = Source.fromResource(s"guid-pools/${info.id}.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
val custom = decode[Seq[GuidNumberPool]](json).toOption.get
|
|
customizePools(defaultGuids, custom)
|
|
} catch {
|
|
case _: Exception => defaultGuids
|
|
}
|
|
|
|
val zone = new Zone(info.id, zoneMaps.find(_.name.equals(info.map.value)).get, info.value) {
|
|
private val addPoolsFunc: () => Unit = addPools(guids, zone = this)
|
|
|
|
override def SetupNumberPools() : Unit = addPoolsFunc()
|
|
|
|
override def init(implicit context: ActorContext): Unit = {
|
|
guids.find { pool => pool.name.equals("projectiles") } match {
|
|
case Some(pool) =>
|
|
(pool.start to pool.max).foreach { map.addLocalObject(_, LocalProjectile.Constructor) }
|
|
case None => ;
|
|
}
|
|
guids.find { pool => pool.name.equals("locker-contents") } match {
|
|
case Some(pool) =>
|
|
(pool.start to pool.max).foreach { map.addLocalObject(_, LocalLockerItem.Constructor) }
|
|
case None => ;
|
|
}
|
|
super.init(context)
|
|
|
|
if (!info.id.startsWith("tz")) {
|
|
this.HotSpotCoordinateFunction = Zones.HotSpots.standardRemapping(info.map.scale, info.map.hotSpotSpan, info.map.hotSpotSpan)
|
|
this.HotSpotTimeFunction = Zones.HotSpots.standardTimeRules
|
|
Zones.initZoneAmenities(zone = this)
|
|
}
|
|
|
|
//special conditions
|
|
//1. sanctuaries are completely owned by a single faction
|
|
//2. set up the third warp gate on sanctuaries to be a broadcast warp gate
|
|
//3. set up sanctuary-linked warp gates on "home continents" (the names make no sense anymore, don't even ask)
|
|
//4. assign the caverns internally
|
|
val bldgs = Buildings.values
|
|
info.id match {
|
|
case "z1" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Solsar_to_Amerish", PlanetSideEmpire.TR)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z2" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Hossin_to_VSSanc", PlanetSideEmpire.TR)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z3" =>
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z4" =>
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z5" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Forseral_to_Solsar", PlanetSideEmpire.VS)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z6" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Ceryshen_to_Hossin", PlanetSideEmpire.VS)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z7" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Esamir_to_VSSanc", PlanetSideEmpire.NC)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z8" =>
|
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
|
case gate: WarpGate => gate.Active = false
|
|
}
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z9" =>
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "z10" =>
|
|
setWarpGateToFactionOwnedAndBroadcast(bldgs, name = "WG_Amerish_to_Solsar", PlanetSideEmpire.NC)
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case "home1" =>
|
|
bldgs.foreach(_.Faction = PlanetSideEmpire.NC)
|
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.NC
|
|
}
|
|
case "home2" =>
|
|
bldgs.foreach(_.Faction = PlanetSideEmpire.TR)
|
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.TR
|
|
}
|
|
case "home3" =>
|
|
bldgs.foreach(_.Faction = PlanetSideEmpire.VS)
|
|
bldgs.filter(_.Name.startsWith("WG_")).map {
|
|
case gate: WarpGate => gate.AllowBroadcastFor = PlanetSideEmpire.VS
|
|
}
|
|
case "i4" =>
|
|
bldgs.find(_.Name.equals("Map96_Gate_Three")).map {
|
|
case gate: WarpGate => gate.Active = false
|
|
}
|
|
case zoneId if zoneId.startsWith("c") =>
|
|
map.cavern = true
|
|
deactivateGeoWarpGateOnContinent(bldgs)
|
|
case _ => ;
|
|
}
|
|
}
|
|
}
|
|
val map = zone.map
|
|
val zoneid = zone.id
|
|
intercontinentalLattice.foreach { obj =>
|
|
obj.asArray.get.foreach { entry =>
|
|
val arr = entry.asArray.get
|
|
val arrHead = arr.head.asString.get
|
|
if (arrHead.startsWith(s"$zoneid/")) {
|
|
map.addLatticeLink(arrHead, arr(1).asString.get)
|
|
}
|
|
}
|
|
}
|
|
zone
|
|
}
|
|
}
|
|
|
|
lazy val cavernLattice = {
|
|
val res = Source.fromResource(s"zonemaps/lattice.json")
|
|
val json = res.mkString
|
|
res.close()
|
|
val jsonObj = parse(json).toOption.get.asObject
|
|
val keys = jsonObj match {
|
|
case Some(jsonToObject) => jsonToObject.keys.filter { _.startsWith("caverns-") }
|
|
case _ => Nil
|
|
}
|
|
val pairs = keys.map { key =>
|
|
(
|
|
key,
|
|
jsonObj.get(key).map { obj =>
|
|
obj.asArray.get.map { entry =>
|
|
val array = entry.asArray.get
|
|
List(array.head.asString.get, array.last.asString.get)
|
|
}
|
|
}.get
|
|
)
|
|
}
|
|
pairs.toMap[String, Iterable[Iterable[String]]]
|
|
}
|
|
|
|
private def deactivateGeoWarpGateOnContinent(buildings: Iterable[Building]): Unit = {
|
|
buildings.filter(_.Name.startsWith(s"GW_")).map {
|
|
case gate: WarpGate => gate.Active = false
|
|
}
|
|
}
|
|
|
|
private def setWarpGateToFactionOwnedAndBroadcast(
|
|
buildings: Iterable[Building],
|
|
name: String,
|
|
faction: PlanetSideEmpire.Value
|
|
) : Unit = {
|
|
buildings.find(_.Name.equals(name)).map {
|
|
case gate: WarpGate =>
|
|
gate.Faction = faction
|
|
gate.AllowBroadcastFor = faction
|
|
}
|
|
}
|
|
|
|
private def customizePools(base: Seq[GuidNumberPool], custom: Seq[GuidNumberPool]): Seq[GuidNumberPool] = {
|
|
val exclude = custom.map { _.name }
|
|
val remainder = base.filterNot { entry => exclude.contains { entry.name } }
|
|
custom ++ remainder
|
|
}
|
|
|
|
private def addPools(guids: Seq[GuidNumberPool], zone: Zone)(): Unit = {
|
|
guids.foreach { entry =>
|
|
zone.AddPool(name = entry.name, (entry.start to entry.max).toList)
|
|
.foreach { _.Selector = entry.getSelector() }
|
|
}
|
|
}
|
|
|
|
def initZoneAmenities(zone: Zone): Unit = {
|
|
initResourceSilos(zone)
|
|
initWarpGates(zone)
|
|
|
|
def initWarpGates(zone: Zone): Unit = {
|
|
// todo: work out which faction owns links to this warpgate and if they should be marked as broadcast or not
|
|
// todo: enable geowarps to go to the correct cave
|
|
zone.Buildings.values.collect {
|
|
case wg: WarpGate
|
|
if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
|
|
wg.Active = true
|
|
wg.Faction = PlanetSideEmpire.NEUTRAL
|
|
case geowarp: WarpGate
|
|
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
|
|
geowarp.Faction = PlanetSideEmpire.NEUTRAL
|
|
geowarp.Active = false
|
|
}
|
|
}
|
|
|
|
def initResourceSilos(zone: Zone): Unit = {
|
|
// todo: load silo charge from database
|
|
// todo: load silo charge from this function call
|
|
// zone.Buildings.values.flatMap {
|
|
// _.Amenities.collect {
|
|
// case silo: ResourceSilo =>
|
|
// silo.Actor ! ResourceSilo.UpdateChargeLevel(silo.MaxNtuCapacitor)
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the zone identifier name for the sanctuary continent of a given empire.
|
|
*
|
|
* @param faction the empire
|
|
* @return the zone id
|
|
*/
|
|
def sanctuaryZoneId(faction: PlanetSideEmpire.Value): String = {
|
|
faction match {
|
|
case PlanetSideEmpire.NC => "home1"
|
|
case PlanetSideEmpire.TR => "home2"
|
|
case PlanetSideEmpire.VS => "home3"
|
|
case _ => throw new UnsupportedOperationException()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the zone number for the sanctuary continent of a given empire.
|
|
*
|
|
* @param faction the empire
|
|
* @return the zone number, within the sequence 1-32
|
|
*/
|
|
def sanctuaryZoneNumber(faction: PlanetSideEmpire.Value): Int = {
|
|
faction match {
|
|
case PlanetSideEmpire.NC => 11
|
|
case PlanetSideEmpire.TR => 12
|
|
case PlanetSideEmpire.VS => 13
|
|
case _ => throw new UnsupportedOperationException()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a zone identification string, provide that zone's ordinal number.
|
|
* As zone identification naming is extremely formulaic,
|
|
* just being able to poll the zone's identifier by its first few letters will produce its ordinal position.
|
|
*
|
|
* @param id a zone id string
|
|
* @return a zone number
|
|
*/
|
|
def numberFromId(id: String): Int = {
|
|
if (id.startsWith("z")) { //z2 -> 2
|
|
id.substring(1).toInt
|
|
} else if (id.startsWith("home")) { //home2 -> 2 + 10 = 12
|
|
id.substring(4).toInt + 10
|
|
} else if (id.startsWith("tz")) { //tzconc -> (14 + (3 * 1) + 2) -> 19
|
|
(List("tr", "nc", "vs").indexOf(id.substring(4)) * 3) + List("sh", "dr", "co").indexOf(id.substring(2, 4)) + 14
|
|
} else if (id.startsWith("c")) { //c2 -> 2 + 21 = 23
|
|
id.substring(1).toInt + 21
|
|
} else if (id.startsWith("i")) { //i2 -> 2 + 28 = 30
|
|
id.substring(1).toInt + 28
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
object HotSpots {
|
|
|
|
import net.psforever.objects.sourcing.SourceEntry
|
|
import net.psforever.objects.zones.MapScale
|
|
import net.psforever.types.Vector3
|
|
|
|
import scala.concurrent.duration._
|
|
|
|
/**
|
|
* Produce hotspot coordinates based on map coordinates.
|
|
*
|
|
* @see `FindClosestDivision`
|
|
* @param scale the map's scale (width and height)
|
|
* @param longDivNum the number of division lines spanning the width of the `scale`
|
|
* @param latDivNum the number of division lines spanning the height of the `scale`
|
|
* @param pos the absolute position of the activity reported
|
|
* @return the position for a hotspot
|
|
*/
|
|
def standardRemapping(scale: MapScale, longDivNum: Int, latDivNum: Int)(pos: Vector3): Vector3 = {
|
|
Vector3(
|
|
//x
|
|
findClosestDivision(pos.x, scale.width, longDivNum.toFloat),
|
|
//y
|
|
findClosestDivision(pos.y, scale.height, latDivNum.toFloat),
|
|
//z is always zero - maps are flat 2D planes
|
|
0
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Produce hotspot coordinates based on map coordinates.<br>
|
|
* <br>
|
|
* Transform a reported number by mapping it
|
|
* into a division from a regular pattern of divisions
|
|
* defined by the scale divided evenly a certain number of times.
|
|
* The depicted number of divisions is actually one less than the parameter number
|
|
* as the first division is used to represent everything before that first division (there is no "zero").
|
|
* Likewise, the last division occurs before the farther edge of the scale is counted
|
|
* and is used to represent everything after that last division.
|
|
* This is not unlike rounding.
|
|
*
|
|
* @param coordinate the point to scale
|
|
* @param scale the map's scale (width and height)
|
|
* @param divisions the number of division lines spanning across the `scale`
|
|
* @return the closest regular division
|
|
*/
|
|
private def findClosestDivision(coordinate: Float, scale: Float, divisions: Float): Float = {
|
|
val divLength: Float = scale / divisions
|
|
if (coordinate >= scale - divLength) {
|
|
scale - divLength
|
|
} else if (coordinate >= divLength) {
|
|
val sector: Float = (coordinate * divisions / scale).toInt * divLength
|
|
val nextSector: Float = sector + divLength
|
|
if (coordinate - sector < nextSector - coordinate) {
|
|
sector
|
|
} else {
|
|
nextSector
|
|
}
|
|
} else {
|
|
divLength
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine a duration for which the hotspot will be displayed on the zone map.
|
|
* Friendly fire is not recognized.
|
|
*
|
|
* @param defender the defending party
|
|
* @param attacker the attacking party
|
|
* @return the duration
|
|
*/
|
|
def standardTimeRules(defender: SourceEntry, attacker: SourceEntry): FiniteDuration = {
|
|
import net.psforever.objects.GlobalDefinitions
|
|
if (attacker.Faction == defender.Faction) {
|
|
0 seconds
|
|
} else {
|
|
//TODO is target occupy-able and occupied, or jammer-able and jammered?
|
|
defender match {
|
|
case _: PlayerSource =>
|
|
60 seconds
|
|
case _: VehicleSource =>
|
|
60 seconds
|
|
case _: DeployableSource =>
|
|
60 seconds
|
|
case _: TurretSource =>
|
|
60 seconds
|
|
case g if g.Definition == GlobalDefinitions.generator =>
|
|
90 seconds
|
|
case _ =>
|
|
0 seconds
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|