mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
Merge remote-tracking branch 'refs/remotes/psforever/master' into AvatarImplantMessage
This commit is contained in:
commit
cbe4689d42
|
|
@ -3,11 +3,12 @@ package net.psforever.objects
|
|||
|
||||
import net.psforever.objects.definition._
|
||||
import net.psforever.objects.definition.converter.{CommandDetonaterConverter, LockerContainerConverter, REKConverter}
|
||||
import net.psforever.objects.serverobject.doors.DoorDefinition
|
||||
import net.psforever.objects.equipment.CItem.DeployedItem
|
||||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.inventory.InventoryTile
|
||||
import net.psforever.objects.terminals.OrderTerminalDefinition
|
||||
import net.psforever.packet.game.objectcreate.ObjectClass
|
||||
import net.psforever.objects.serverobject.locks.IFFLockDefinition
|
||||
import net.psforever.objects.serverobject.terminals.{CertTerminalDefinition, OrderTerminalDefinition}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
object GlobalDefinitions {
|
||||
|
|
@ -1239,5 +1240,12 @@ object GlobalDefinitions {
|
|||
fury.TrunkOffset = 30
|
||||
|
||||
val
|
||||
orderTerminal = new OrderTerminalDefinition
|
||||
order_terminal = new OrderTerminalDefinition
|
||||
val
|
||||
cert_terminal = new CertTerminalDefinition
|
||||
|
||||
val
|
||||
lock_external = new IFFLockDefinition
|
||||
val
|
||||
door = new DoorDefinition
|
||||
}
|
||||
|
|
|
|||
96
common/src/main/scala/net/psforever/objects/ObjectType.scala
Normal file
96
common/src/main/scala/net/psforever/objects/ObjectType.scala
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
object ObjectType extends Enumeration {
|
||||
type Value = String
|
||||
|
||||
val AmbientSoundSource = "ambient_sound_source"
|
||||
val Ammunition = "ammunition"
|
||||
val AnimatedBarrier = "animated_barrier"
|
||||
val Applicator = "applicator"
|
||||
val Armor = "armor"
|
||||
val ArmorSiphon = "armor_siphon"
|
||||
val AwardStatistic = "award_statistic"
|
||||
val Avatar = "avatar"
|
||||
val AvatarBot = "avatar_bot"
|
||||
val Ball = "ball"
|
||||
val Bank = "bank"
|
||||
val Barrier = "barrier"
|
||||
val BfrTerminal = "bfr_terminal"
|
||||
val Billboard = "billboard"
|
||||
val Boomer = "boomer"
|
||||
val BoomerTrigger = "boomer_trigger"
|
||||
val Building = "building"
|
||||
val CaptureFlag = "capture_flag"
|
||||
val CaptureFlagSocket = "capture_flag_socket"
|
||||
val CaptureTerminal = "capture_terminal"
|
||||
val CertTerminal = "cert_terminal"
|
||||
val ChainLashDamager = "chain_lash_damager"
|
||||
val Dispenser = "dispenser"
|
||||
val Door = "door"
|
||||
val EmpBlast = "emp_blast"
|
||||
val FrameVehicle = "framevehicle"
|
||||
val Flag = "flag"
|
||||
val FlightVehicle = "flightvehicle"
|
||||
val ForceDome = "forcedome"
|
||||
val ForceDomeGenerator = "forcedomegenerator"
|
||||
val Game = "game"
|
||||
val Generic = "generic"
|
||||
val GenericTeleportion = "generic_teleportation"
|
||||
val GeneratorTerminal = "generator_terminal"
|
||||
val GsGenbase = "GS_genbase"
|
||||
val HandGrenade = "hand_grenade"
|
||||
val HeMine = "he_mine"
|
||||
val HeavyWeapon = "heavy_weapon"
|
||||
val HoverVehicle = "hovervehicle"
|
||||
val Implant = "implant"
|
||||
val ImplantInterfaceTerminal = "implant_terminal_interface"
|
||||
val Lazer = "lazer"
|
||||
val Locker = "locker"
|
||||
val LockerContainer = "locker_container"
|
||||
val LockExternal = "lock_external"
|
||||
val LockSmall = "lock_small"
|
||||
val MainTerminal = "main_terminal"
|
||||
val Map = "map"
|
||||
val MedicalTerminal = "medical_terminal"
|
||||
val Medkit = "medkit"
|
||||
val Monolith = "monolith"
|
||||
val MonolithUnit = "monolith_unit"
|
||||
val MotionAlarmSensorDest = "motion_alarm_sensor_dest"
|
||||
val NanoDispenser = "nano_dispenser"
|
||||
val NtuSipon = "ntu_siphon"
|
||||
val OrbitalShuttlePad = "orbital_shuttle_pad"
|
||||
val OrbitalStrike = "orbital_strike"
|
||||
val OrderTerminal = "order_terminal"
|
||||
val PainTerminal = "pain_terminal"
|
||||
val Projectile = "projectile"
|
||||
val RadiationCloud = "radiation_cloud"
|
||||
val RearmTerminal = "rearm_terminal"
|
||||
val RechargeTerminal = "recharge_terminal"
|
||||
val Rek = "rek"
|
||||
val RepairTerminal = "repair_terminal"
|
||||
val ResourceSilo = "resource_silo"
|
||||
val RespawnTube = "respawn_tube"
|
||||
val SensorShield = "sensor_shield"
|
||||
val ShieldGenerator = "shield_generator"
|
||||
val Shifter = "shifter"
|
||||
val SkyDome = "skydome"
|
||||
val SpawnPlayer = "spawn_player"
|
||||
val SpawnPoint = "spawn_point"
|
||||
val SpawnTerminal = "spawn_terminal"
|
||||
val TeleportPad = "teleport_pad"
|
||||
val Terminal = "terminal"
|
||||
val TradeContainer = "trade_container"
|
||||
val UplinkDevice = "uplink_device"
|
||||
val VanuCradleClass = "vanu_cradle_class"
|
||||
val VanuModuleClass = "vanu_module_class"
|
||||
val VanuModuleFactory = "vanu_module_factory"
|
||||
val VanuReceptacleClass = "vanu_receptacle_class"
|
||||
val Vehicle = "vehicle"
|
||||
val VehicleCreationPad = "vehicle_creation_pad"
|
||||
val VehicleLandingPad = "vehicle_landing_pad"
|
||||
val VehicleTerminal = "vehicle_terminal"
|
||||
val Warpgate = "waprgate"
|
||||
val WarpZone = "warp_zone"
|
||||
val Weapon = "weapon"
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ import scala.collection.mutable
|
|||
class Player(private val name : String,
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
private val sex : CharacterGender.Value,
|
||||
private val voice : Int,
|
||||
private val head : Int
|
||||
private val head : Int,
|
||||
private val voice : Int
|
||||
) extends PlanetSideGameObject {
|
||||
private var alive : Boolean = false
|
||||
private var backpack : Boolean = false
|
||||
|
|
@ -64,8 +64,6 @@ class Player(private val name : String,
|
|||
/** Last medkituse. */
|
||||
var lastMedkit : Long = 0
|
||||
var death_by : Int = 0
|
||||
var doors : Array[Int] = Array.ofDim(120)
|
||||
var doorsTime : Array[Long] = Array.ofDim(120)
|
||||
var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L)
|
||||
var lastShotSeq_time : Int = -1
|
||||
/** The player is shooting. */
|
||||
|
|
@ -521,11 +519,11 @@ object Player {
|
|||
final val FreeHandSlot : Int = 250
|
||||
final val HandsDownSlot : Int = 255
|
||||
|
||||
def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
|
||||
new Player(name, faction, sex, voice, head)
|
||||
def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = {
|
||||
new Player(name, faction, sex, head, voice)
|
||||
}
|
||||
|
||||
def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
|
||||
def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = {
|
||||
val obj = new Player(name, faction, sex, voice, head)
|
||||
obj.GUID = guid
|
||||
obj
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects.definition.converter
|
|||
|
||||
import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, Cosmetics, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
|
||||
import net.psforever.types.{GrenadeState, ImplantType}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
|
@ -42,6 +42,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
MakeImplantEntries(obj),
|
||||
List.empty[String], //TODO fte list
|
||||
List.empty[String], //TODO tutorial list
|
||||
MakeCosmetics(obj.BEP),
|
||||
InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
|
|
@ -56,7 +57,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
|
||||
CharacterAppearanceData(
|
||||
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
|
||||
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Voice, obj.Head),
|
||||
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice),
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
|
|
@ -132,7 +133,10 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
* @see `ImplantEntry` in `DetailedCharacterData`
|
||||
*/
|
||||
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
|
||||
obj.Implants.map(slot => {
|
||||
val numImplants : Int = DetailedCharacterData.numberOfImplantSlots(obj.BEP)
|
||||
val implants = obj.Implants
|
||||
(0 until numImplants).map(index => {
|
||||
val slot = implants(index)
|
||||
slot.Installed match {
|
||||
case Some(_) =>
|
||||
if(slot.Initialized) {
|
||||
|
|
@ -176,6 +180,20 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should this player be of battle rank 24 or higher, they will have a mandatory cosmetics object.
|
||||
* @param bep battle experience points
|
||||
* @see `Cosmetics`
|
||||
* @return the `Cosmetics` options
|
||||
*/
|
||||
protected def MakeCosmetics(bep : Long) : Option[Cosmetics] =
|
||||
if(DetailedCharacterData.isBR24(bep)) {
|
||||
Some(Cosmetics(false, false, false, false, false))
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
|
||||
* The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars.
|
||||
|
|
@ -236,7 +254,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
* @param equip the game object
|
||||
* @return the game object in decoded packet form
|
||||
*/
|
||||
private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = {
|
||||
protected def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = {
|
||||
InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get)
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +292,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
* @param obj the `Player` game object
|
||||
* @return the holster's Enumeration value
|
||||
*/
|
||||
private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = {
|
||||
protected def GetDrawnSlot(obj : Player) : DrawnSlot.Value = {
|
||||
try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.definition.converter
|
||||
|
||||
import net.psforever.objects.{EquipmentSlot, Player}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars}
|
||||
import net.psforever.types.{GrenadeState, ImplantType}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* `CharacterSelectConverter` is a simplified `AvatarConverter`
|
||||
* that is tailored for appearance of the player character on the character selection screen.
|
||||
* Details that would not be apparent on that screen such as implants or certifications are ignored.
|
||||
*/
|
||||
class CharacterSelectConverter extends AvatarConverter {
|
||||
override def ConstructorData(obj : Player) : Try[CharacterData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData"))
|
||||
|
||||
override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = {
|
||||
Success(
|
||||
DetailedCharacterData(
|
||||
MakeAppearanceData(obj),
|
||||
obj.BEP,
|
||||
obj.CEP,
|
||||
1, 1, 0, 1, 1,
|
||||
Nil,
|
||||
MakeImplantEntries(obj), //necessary for correct stream length
|
||||
Nil, Nil,
|
||||
MakeCosmetics(obj.BEP),
|
||||
InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)),
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`.
|
||||
* @param obj the `Player` game object
|
||||
* @see `AvatarConverter.MakeAppearanceData`
|
||||
* @return the resulting `CharacterAppearanceData`
|
||||
*/
|
||||
private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
|
||||
CharacterAppearanceData(
|
||||
PlacementData(0f, 0f, 0f),
|
||||
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, 1),
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
obj.ExoSuit,
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
0f,
|
||||
0f,
|
||||
true,
|
||||
GrenadeState.None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
RibbonBars()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an `Array` of `Implant` objects into a `List` of `ImplantEntry` objects suitable as packet data.
|
||||
* @param obj the `Player` game object
|
||||
* @return the resulting implant `List`
|
||||
* @see `ImplantEntry` in `DetailedCharacterData`
|
||||
*/
|
||||
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
|
||||
List.fill[ImplantEntry](DetailedCharacterData.numberOfImplantSlots(obj.BEP))(ImplantEntry(ImplantType.None, None))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||
* @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters
|
||||
* @param list the current `List` of transformed data
|
||||
* @param index which holster is currently being explored
|
||||
* @see `AvatarConverter.recursiveMakeHolsters`
|
||||
* @return the `List` of inventory data created from the holsters
|
||||
*/
|
||||
@tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
|
||||
if(!iter.hasNext) {
|
||||
list
|
||||
}
|
||||
else {
|
||||
val slot : EquipmentSlot = iter.next
|
||||
if(slot.Equipment.isDefined) {
|
||||
val equip : Equipment = slot.Equipment.get
|
||||
recursiveMakeHolsters(
|
||||
iter,
|
||||
list :+ BuildDetailedEquipment(index, equip),
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
else {
|
||||
recursiveMakeHolsters(iter, list, index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,14 +60,18 @@ class NumberPoolHub(private val source : NumberSource) {
|
|||
* @param name the name of the pool
|
||||
* @param pool the `List` of numbers that will belong to the pool
|
||||
* @return the newly-created number pool
|
||||
* @throws IllegalArgumentException if the pool is already defined;
|
||||
* if the pool contains numbers the source does not
|
||||
* @throws IllegalArgumentException if the pool's name is already defined;
|
||||
* if the pool is (already) empty;
|
||||
* if the pool contains numbers the source does not;
|
||||
* if the pool contains numbers from already existing pools
|
||||
*/
|
||||
def AddPool(name : String, pool : List[Int]) : NumberPool = {
|
||||
if(hash.get(name).isDefined) {
|
||||
throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?")
|
||||
}
|
||||
if(pool.isEmpty) {
|
||||
throw new IllegalArgumentException(s"can not add empty pool $name")
|
||||
}
|
||||
if(source.Size <= pool.max) {
|
||||
throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size")
|
||||
}
|
||||
|
|
@ -203,8 +207,8 @@ class NumberPoolHub(private val source : NumberSource) {
|
|||
val slctr = pool.Selector
|
||||
import net.psforever.objects.guid.selector.SpecificSelector
|
||||
val specific = new SpecificSelector
|
||||
specific.SelectionIndex = number
|
||||
pool.Selector = specific
|
||||
specific.SelectionIndex = number
|
||||
pool.Get()
|
||||
pool.Selector = slctr
|
||||
register_GetAvailableNumberFromSource(number)
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
|
||||
/**
|
||||
* A message for requesting information about the registration status of an object or a number.
|
||||
* @param obj the optional object
|
||||
* @param number the optional number
|
||||
*/
|
||||
final case class IsRegistered(obj : Option[IdentifiableEntity], number : Option[Int])
|
||||
|
||||
object IsRegistered {
|
||||
/**
|
||||
* Overloaded constructor for querying an object's status.
|
||||
* @param obj the object
|
||||
* @return an `IsRegistered` object
|
||||
*/
|
||||
def apply(obj : IdentifiableEntity) : IsRegistered = {
|
||||
new IsRegistered(Some(obj), None)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded constructor for querying a number's status.
|
||||
* @param number the number
|
||||
* @return an `IsRegistered` object
|
||||
*/
|
||||
def apply(number : Int) : IsRegistered = {
|
||||
new IsRegistered(None, Some(number))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
import net.psforever.objects.guid.pool.NumberPool
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* An `Actor` that wraps around the `Actor` for a `NumberPool` and automates a portion of the number registration process.<br>
|
||||
* <br>
|
||||
* The `NumberPoolActor` that is created is used as the synchronized "gate" through which the number selection process occurs.
|
||||
* This `Actor` `ask`s the internal `Actor` and then waits on that `Future` to resolve.
|
||||
* For the registration process, once it resolves, a number for the accompanying object has been chosen.
|
||||
* The last part involves configuring the `NumberSource` of the hub so that it knows.
|
||||
* For the process of revoking registration, the number from the object is returned to the pool.
|
||||
* Like during the registration process, the `NumberSource` is then also updated.<br>
|
||||
* <br>
|
||||
* The object is always registered using the underlying governed `NumberPool`.
|
||||
* The object will not unregister if the object or its number are not recognized as members previously registered to the `NumberPool`.<br>
|
||||
* Whether or not an object or a specific number has been registered is always possible.
|
||||
* The scope encompasses the whole of the associated `NumberSource` as opposed to just this `NumberPool`.
|
||||
* @param hub the `NumberPoolHub` this `Actor` manipulates
|
||||
* @param pool the specific `NumberPool` this `Actor` maintains
|
||||
* @param poolActor a shared `Actor` that governs this `NumberPool`
|
||||
*/
|
||||
class NumberPoolAccessorActor(private val hub : NumberPoolHub, private val pool : NumberPool, private val poolActor : ActorRef) extends Actor {
|
||||
//the timeout is for when we ask the poolActor
|
||||
private implicit val timeout = Timeout(50 milliseconds)
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
private final case class GUIDRequest(obj : IdentifiableEntity, replyTo : ActorRef)
|
||||
private val requestQueue : collection.mutable.LongMap[GUIDRequest] = new collection.mutable.LongMap()
|
||||
private var index : Long = Long.MinValue
|
||||
|
||||
def receive : Receive = {
|
||||
//register
|
||||
case Register(obj, _, None, call) =>
|
||||
try {
|
||||
obj.GUID //stop if object has a GUID; sometimes this happens
|
||||
log.warn(s"$obj already registered")
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
val id : Long = index
|
||||
index += 1
|
||||
requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
|
||||
poolActor ! NumberPoolActor.GetAnyNumber(Some(id))
|
||||
}
|
||||
|
||||
case Register(obj, _, Some(number), call) =>
|
||||
try {
|
||||
obj.GUID //stop if object has a GUID; sometimes this happens
|
||||
log.warn(s"$obj already registered")
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
val id : Long = index
|
||||
index += 1
|
||||
requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
|
||||
poolActor ! NumberPoolActor.GetSpecificNumber(number, Some(id))
|
||||
}
|
||||
|
||||
case NumberPoolActor.GiveNumber(number, id) =>
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
Register(nid, requestQueue.remove(nid), number)
|
||||
case _ =>
|
||||
pool.Return(number) //recovery?
|
||||
log.warn(s"received a number but there is no request to process it; returning number to pool")
|
||||
}
|
||||
|
||||
case NumberPoolActor.NoNumber(ex, id) =>
|
||||
val req = id match {
|
||||
case Some(nid : Long) =>
|
||||
val req = requestQueue.remove(nid)
|
||||
if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
|
||||
case _ =>
|
||||
"generic request;" //should be unreachable
|
||||
}
|
||||
log.warn(s"a number was not drawn from the pool; $req $ex")
|
||||
|
||||
//unregister
|
||||
case Unregister(obj, call) =>
|
||||
val callback = call.getOrElse(sender())
|
||||
try {
|
||||
val number = obj.GUID.guid
|
||||
if(pool.Numbers.contains(number) && hub.WhichPool(obj).isDefined) {
|
||||
val id : Long = index
|
||||
index += 1
|
||||
requestQueue += id -> GUIDRequest(obj, callback)
|
||||
poolActor ! NumberPoolActor.ReturnNumber(number, Some(id))
|
||||
}
|
||||
else {
|
||||
callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool"))
|
||||
}
|
||||
}
|
||||
catch {
|
||||
case msg : Exception =>
|
||||
callback ! Failure(msg)
|
||||
}
|
||||
|
||||
case NumberPoolActor.ReturnNumberResult(number, None, id) =>
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
Unregister(nid, requestQueue.remove(nid), number)
|
||||
case _ =>
|
||||
NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
|
||||
log.error(s"returned a number but there is no request to process it; recovering the number from pool")
|
||||
}
|
||||
|
||||
case NumberPoolActor.ReturnNumberResult(number, ex, id) =>
|
||||
val req = id match {
|
||||
case Some(nid : Long) =>
|
||||
val req = requestQueue.remove(nid)
|
||||
if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
|
||||
case _ =>
|
||||
"generic request;" //should be unreachable
|
||||
}
|
||||
log.warn(s"a number $number was not returned to the pool; $req $ex")
|
||||
|
||||
//common
|
||||
case IsRegistered(Some(obj), None) =>
|
||||
sender ! hub.isRegistered(obj)
|
||||
|
||||
case IsRegistered(None, Some(number)) =>
|
||||
sender ! hub.isRegistered(number)
|
||||
|
||||
case NumberPoolActor.ReturnNumber(number, _) =>
|
||||
sender ! (poolActor ? NumberPoolActor.ReturnNumber(number))
|
||||
|
||||
case msg =>
|
||||
log.warn(s"unexpected message received - $msg")
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* If there is a successful request object to be found, complete the registration request.
|
||||
* @param id the identifier of this request
|
||||
* @param request the request data
|
||||
* @param number the number that was drawn from the `NumberPool`
|
||||
*/
|
||||
private def Register(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
|
||||
request match {
|
||||
case Some(GUIDRequest(obj, replyTo)) =>
|
||||
processRegisterResult(obj, number, replyTo)
|
||||
case None =>
|
||||
pool.Return(number) //recovery?
|
||||
log.warn(s"received a number but the request for it is missing; returning number to pool")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* This step completes the registration by consulting the `NumberSource`.
|
||||
* @param obj the object
|
||||
* @param number the number to use
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def processRegisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
|
||||
try {
|
||||
obj.GUID
|
||||
pool.Return(number) //recovery?
|
||||
callback ! Success(obj)
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
hub.latterPartRegister(obj, number) match {
|
||||
case Success(_) =>
|
||||
callback ! Success(obj)
|
||||
case Failure(ex) =>
|
||||
pool.Return(number) //recovery?
|
||||
callback ! Failure(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object un-registration process.
|
||||
* If there is a successful request object to be found, complete the registration request.
|
||||
* @param id the identifier of this request
|
||||
* @param request the request data
|
||||
* @param number the number that was drawn from the `NumberPool`
|
||||
*/
|
||||
private def Unregister(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
|
||||
request match {
|
||||
case Some(GUIDRequest(obj, replyTo)) =>
|
||||
processUnregisterResult(obj, obj.GUID.guid, replyTo)
|
||||
case None =>
|
||||
NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
|
||||
log.error(s"returned a number but the rest of the request is missing; recovering the number from pool")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object un-registration process.
|
||||
* This step completes revoking the object's registration by consulting the `NumberSource`.
|
||||
* @param obj the object
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def processUnregisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
|
||||
hub.latterPartUnregister(number) match {
|
||||
case Some(_) =>
|
||||
obj.Invalidate()
|
||||
callback ! Success(obj)
|
||||
case None =>
|
||||
NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
|
||||
callback ! Failure(new Exception(s"failed to unregister a number; this may be a critical error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import akka.actor.{Actor, ActorRef, Props}
|
||||
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
import net.psforever.objects.guid.pool.NumberPool
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* An incoming message for retrieving a specific `NumberPoolAccessorActor`.
|
||||
* @param name the name of the accessor's `NumberPool`
|
||||
*/
|
||||
final case class RequestPoolActor(name : String)
|
||||
|
||||
/**
|
||||
* An outgoing message for giving a specific `NumberPoolAccessorActor`.
|
||||
* @param name the name of the accessor's `NumberPool`, for reference
|
||||
* @param actor the accessor
|
||||
*/
|
||||
final case class DeliverPoolActor(name : String, actor : ActorRef)
|
||||
|
||||
/**
|
||||
* An `Actor` that wraps around the management system for `NumberPools`.<br>
|
||||
* <br>
|
||||
* By just instantiating, this object builds and stores a `NumberPoolAccessorActor` for each `NumberPool` known to the `hub`.
|
||||
* Additional `NumberPool`s created by the `hub` need to be paired with a created accessor manually.
|
||||
* Each accessor is the primary entry point to a registration process for the specific `NumberPool` it represents.
|
||||
* The `hub` `Actor` itself distribute any registration task it receives out to an applicable accessor of which it is aware.
|
||||
* It will attempt to revoke registration on its own, without relying on the functionality from any accessor.<br>
|
||||
* <br>
|
||||
* In the same way that `NumberPoolHub` is a tool for keeping track of `NumberPool` objects,
|
||||
* its `Actor` is a tool for keeping track of accessors created from `NumberPool` objects.
|
||||
* It is very, however, for handling unspecific revoke tasks.
|
||||
* @param hub the central `NumberPool` management object for an embedded `NumberSource` object
|
||||
*/
|
||||
class NumberPoolHubActor(private val hub : NumberPoolHub) extends Actor {
|
||||
private val actorHash : mutable.HashMap[String, ActorRef] = mutable.HashMap[String, ActorRef]()
|
||||
hub.Pools.foreach({ case(name, pool) => CreatePoolActor(name, pool) })
|
||||
implicit val timeout = Timeout(50 milliseconds)
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
def receive : Receive = {
|
||||
case RequestPoolActor(name) =>
|
||||
sender ! (GetPoolActor(name) match {
|
||||
case Success(poolActor) =>
|
||||
DeliverPoolActor(name, poolActor)
|
||||
case Failure(ex) =>
|
||||
Failure(ex)
|
||||
})
|
||||
|
||||
case Register(obj, name, None, callback) =>
|
||||
HubRegister(obj, name, callback)
|
||||
|
||||
case Register(obj, name, Some(number), callback) =>
|
||||
HubRegister(obj, name, number, callback)
|
||||
|
||||
//common
|
||||
case IsRegistered(Some(obj), None) =>
|
||||
sender ! hub.isRegistered(obj)
|
||||
|
||||
case IsRegistered(None, Some(number)) =>
|
||||
sender ! hub.isRegistered(number)
|
||||
|
||||
case Unregister(obj, callback) =>
|
||||
Unregister(obj, if(callback.isEmpty) { sender } else { callback.get })
|
||||
|
||||
case msg =>
|
||||
log.warn(s"unexpected message received - ${msg.toString}")
|
||||
}
|
||||
|
||||
/**
|
||||
* From a name, find an existing `NumberPoolAccessorActor`.
|
||||
* @param name the accessor's name
|
||||
* @return the accessor that was requested
|
||||
*/
|
||||
private def GetPoolActor(name : String) : Try[ActorRef] = {
|
||||
actorHash.get(name) match {
|
||||
case Some(actor) =>
|
||||
Success(actor)
|
||||
case _ =>
|
||||
Failure(new Exception(s"number pool $name not defined"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new `NumberPoolAccessorActor` and add it to the local collection of accessors.
|
||||
* @param name the accessor's name
|
||||
* @param pool the underlying `NumberPool`
|
||||
*/
|
||||
private def CreatePoolActor(name : String, pool : NumberPool) : Unit = {
|
||||
actorHash.get(name) match {
|
||||
case None =>
|
||||
actorHash += name -> context.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool), s"${name}Actor")
|
||||
case Some(_) =>
|
||||
//TODO complain?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* Select a valid `NumberPoolAccessorActor` and pass a task onto it.
|
||||
* @param obj an object
|
||||
* @param name a potential accessor pool
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def HubRegister(obj : IdentifiableEntity, name : Option[String], callback : Option[ActorRef]) : Unit = {
|
||||
val genericPool = actorHash("generic")
|
||||
val pool = if(name.isDefined) { actorHash.get(name.get).orElse(Some(genericPool)).get } else { genericPool }
|
||||
pool ! Register(obj, None, None, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* Determine to which `NumberPool` the `number` belongs.
|
||||
* @param obj an object
|
||||
* @param name a potential accessor pool
|
||||
* @param number a potential number
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def HubRegister(obj : IdentifiableEntity, name : Option[String], number : Int, callback : Option[ActorRef]) : Unit = {
|
||||
hub.WhichPool(number) match {
|
||||
case Some(poolname) =>
|
||||
HubRegister_GetActor(obj, name, poolname, number, callback)
|
||||
case None =>
|
||||
self ! Register(obj, name, None, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* Pass a task onto an accessor or, if the accessor can not be found, attempt to recover.
|
||||
* @param obj an object
|
||||
* @param name a potential accessor pool
|
||||
* @param poolname the suggested accessor pool
|
||||
* @param number a potential number
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def HubRegister_GetActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
|
||||
actorHash.get(poolname) match {
|
||||
case Some(pool) =>
|
||||
pool ! Register(obj, None, Some(number), callback)
|
||||
case None =>
|
||||
HubRegister_MissingActor(obj, name, poolname, number, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* If an accessor could not be found in the last step, attempt to create the accessor.
|
||||
* If the accessor can not be created, the `number` can not be used;
|
||||
* fall back on the original pool (`name`).
|
||||
* @param obj an object
|
||||
* @param name a potential accessor pool
|
||||
* @param poolname the suggested accessor pool
|
||||
* @param number a potential number
|
||||
* @param callback an optional callback `ActorRef`
|
||||
*/
|
||||
private def HubRegister_MissingActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
|
||||
hub.GetPool(poolname) match {
|
||||
case Some(pool) =>
|
||||
CreatePoolActor(poolname, pool)
|
||||
actorHash(poolname) ! Register(obj, None, Some(number), callback)
|
||||
case None =>
|
||||
log.error(s"matched number $number to pool $poolname, but could not find $poolname when asked")
|
||||
self ! Register(obj, name, None, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object un-registration process.
|
||||
* This step locates the `NumberPool` to which this object is a member.
|
||||
* If found, it prepares a `Future` to resolve later regarding whether the `NumberPool` accepted the number.
|
||||
* @param obj the object
|
||||
* @param callback a callback `ActorRef`
|
||||
*/
|
||||
private def Unregister(obj : IdentifiableEntity, callback : ActorRef) : Unit = {
|
||||
hub.WhichPool(obj) match {
|
||||
case Some(name) =>
|
||||
val objToUnregister = obj
|
||||
val poolName = name
|
||||
processUnregisterResult(objToUnregister, (actorHash(poolName) ? NumberPoolActor.ReturnNumber(objToUnregister.GUID.guid)).mapTo[Boolean], callback)
|
||||
case None =>
|
||||
callback ! UnregisterFailure(obj, new Exception("could not find pool object is member of"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object un-registration process.
|
||||
* This step completes revoking the object's registration by consulting the `NumberSource`.
|
||||
* @param obj the object
|
||||
* @param result whether the number was returned in the last step
|
||||
* @param callback a callback `ActorRef`
|
||||
*/
|
||||
private def processUnregisterResult(obj : IdentifiableEntity, result : Future[Boolean], callback : ActorRef) : Unit = {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
result.foreach {
|
||||
case true =>
|
||||
hub.latterPartUnregister(obj.GUID.guid)
|
||||
callback ! UnregisterSuccess(obj)
|
||||
case false =>
|
||||
callback ! UnregisterFailure(obj, new Exception("could not find object to remove"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import akka.actor.{Actor, ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* An `Actor` that wraps around converted `NumberPool`s and synchronizes a portion of the number registration process.
|
||||
* The ultimate goal is to manage a coherent group of unique identifiers for a given "region" (`Zone`).
|
||||
* Both parts of the UID system sit atop the `Zone` for easy external access.
|
||||
* The plain part - the `NumberPoolHub` here - is used for low-priority requests such as checking for existing associations.
|
||||
* This `Actor` is the involved portion that paces registration and unregistration.<br>
|
||||
* <br>
|
||||
* A four part process is used for object registration tasks.
|
||||
* First, the requested `NumberPool` is located among the list of known `NumberPool`s.
|
||||
* Second, an asynchronous request is sent to that pool to retrieve a number.
|
||||
* (Only any number. Only a failing case allows for selection of a specific number.)
|
||||
* Third, the asynchronous request returns and the original information about the request is recovered.
|
||||
* Fourth, both sides of the contract are completed by the object being assigned the number and
|
||||
* the underlying "number source" is made to remember an association between the object and the number.
|
||||
* Short circuits and recoveries as available on all steps though reporting is split between logging and callbacks.
|
||||
* The process of removing the association between a number and object (unregistering) is a similar four part process.<br>
|
||||
* <br>
|
||||
* The important relationship between this `Actor` and the `Map` of `NumberPoolActors` is an "gate."
|
||||
* A single `Map` is constructed and shared between multiple entry points to the UID system where requests are messaged.
|
||||
* Multiple entry points send messages to the same `NumberPool`.
|
||||
* That `NumberPool` deals with the messages one at a time and sends reply to each entry point that communicated with it.
|
||||
* This process is almost as fast as the process of the `NumberPool` selecting a number.
|
||||
* (At least, both should be fast.)
|
||||
* @param guid the `NumberPoolHub` that is partially manipulated by this `Actor`
|
||||
* @param poolActors a common mapping created from the `NumberPool`s in `guid`;
|
||||
* there is currently no check for this condition save for requests failing
|
||||
*/
|
||||
class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActors : Map[String, ActorRef]) extends Actor {
|
||||
/** Information about Register and Unregister requests that persists between messages to a specific `NumberPool`. */
|
||||
private val requestQueue : collection.mutable.LongMap[UniqueNumberSystem.GUIDRequest] = new collection.mutable.LongMap()
|
||||
/** The current value for the next request entry's index. */
|
||||
private var index : Long = Long.MinValue
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
def receive : Receive = {
|
||||
case Register(obj, Some(pname), None, call) =>
|
||||
val callback = call.getOrElse(sender())
|
||||
try {
|
||||
obj.GUID //stop if object already has a GUID; sometimes this happens
|
||||
AlreadyRegistered(obj, pname)
|
||||
callback ! Success(obj)
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
val id : Long = index
|
||||
index += 1
|
||||
requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
|
||||
RegistrationProcess(pname, id)
|
||||
}
|
||||
|
||||
//this message is automatically sent by NumberPoolActor
|
||||
case NumberPoolActor.GiveNumber(number, id) =>
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
RegistrationProcess(requestQueue.remove(nid), number, nid)
|
||||
case _ =>
|
||||
log.warn(s"received a number but there is no request to process it; returning number to pool")
|
||||
NoCallbackReturnNumber(number) //recovery?
|
||||
//no callback is possible
|
||||
}
|
||||
|
||||
//this message is automatically sent by NumberPoolActor
|
||||
case NumberPoolActor.NoNumber(ex, id) =>
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
requestQueue.remove(nid) match {
|
||||
case Some(entry) =>
|
||||
entry.replyTo ! Failure(ex) //ONLY callback that is possible
|
||||
case None => ;
|
||||
log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request
|
||||
}
|
||||
case None => ;
|
||||
log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request
|
||||
case _ => ;
|
||||
log.warn(s"unrecognized request $id accompanying a failed number request - $ex")
|
||||
}
|
||||
|
||||
case Unregister(obj, call) =>
|
||||
val callback = call.getOrElse(sender())
|
||||
try {
|
||||
val number = obj.GUID.guid
|
||||
guid.WhichPool(number) match {
|
||||
case Some(pname) =>
|
||||
val id : Long = index
|
||||
index += 1
|
||||
requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
|
||||
UnregistrationProcess(pname, number, id)
|
||||
case None =>
|
||||
callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool"))
|
||||
}
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
log.info(s"$obj is already unregistered")
|
||||
callback ! Success(obj)
|
||||
}
|
||||
|
||||
//this message is automatically sent by NumberPoolActor
|
||||
case NumberPoolActor.ReturnNumberResult(number, None, id) =>
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
UnregistrationProcess(requestQueue.remove(nid), number, nid)
|
||||
case _ =>
|
||||
log.error(s"returned a number but there is no request to process it; recovering the number from pool")
|
||||
NoCallbackGetSpecificNumber(number) //recovery?
|
||||
//no callback is possible
|
||||
}
|
||||
|
||||
//this message is automatically sent by NumberPoolActor
|
||||
case NumberPoolActor.ReturnNumberResult(number, Some(ex), id) => //if there is a problem when returning the number
|
||||
id match {
|
||||
case Some(nid : Long) =>
|
||||
requestQueue.remove(nid) match {
|
||||
case Some(entry) =>
|
||||
entry.replyTo ! Failure(new Exception(s"for ${entry.target} with number $number, ${ex.getMessage}"))
|
||||
case None => ;
|
||||
log.error(s"could not find original request $nid that caused error $ex, but pool was $sender")
|
||||
//no callback is possible
|
||||
}
|
||||
case _ => ;
|
||||
log.error(s"could not find original request $id that caused error $ex, but pool was $sender")
|
||||
//no callback is possible
|
||||
}
|
||||
|
||||
case msg =>
|
||||
log.warn(s"unexpected message received - $msg")
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* Send a message to the `NumberPool` to request a number back.
|
||||
* @param poolName the pool to which the object is trying to register
|
||||
* @param id a potential identifier to associate this request
|
||||
*/
|
||||
private def RegistrationProcess(poolName : String, id : Long) : Unit = {
|
||||
poolActors.get(poolName) match {
|
||||
case Some(pool) =>
|
||||
pool ! NumberPoolActor.GetAnyNumber(Some(id))
|
||||
case None =>
|
||||
//do not log; use callback
|
||||
requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was registered"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* If there is a successful request object to be found, continue the registration request.
|
||||
* @param request the original request data
|
||||
* @param number the number that was drawn from a `NumberPool`
|
||||
*/
|
||||
private def RegistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = {
|
||||
request match {
|
||||
case Some(entry) =>
|
||||
processRegisterResult(entry, number)
|
||||
case None =>
|
||||
log.error(s"returned a number but the rest of the request is missing (id:$id)")
|
||||
if(id != Long.MinValue) { //check to ignore endless loop of error-catching
|
||||
log.warn("returning number to pool")
|
||||
NoCallbackReturnNumber(number) //recovery?
|
||||
//no callback is possible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object registration process.
|
||||
* This step completes the registration by asking the `NumberPoolHub` to sort out its `NumberSource`.
|
||||
* @param entry the original request data
|
||||
* @param number the number to use
|
||||
*/
|
||||
private def processRegisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = {
|
||||
val obj = entry.target
|
||||
guid.latterPartRegister(obj, number) match {
|
||||
case Success(_) =>
|
||||
entry.replyTo ! Success(obj)
|
||||
case Failure(ex) =>
|
||||
//do not log; use callback
|
||||
NoCallbackReturnNumber(number, entry.targetPool) //recovery?
|
||||
entry.replyTo ! Failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object unregistration process.
|
||||
* Send a message to the `NumberPool` to restore the availability of one of its numbers.
|
||||
* @param poolName the pool to which the number will try to be returned
|
||||
* @param number the number that was previously drawn from the specified `NumberPool`
|
||||
* @param id a potential identifier to associate this request
|
||||
*/
|
||||
private def UnregistrationProcess(poolName : String, number : Int, id : Long) : Unit = {
|
||||
poolActors.get(poolName) match {
|
||||
case Some(pool) =>
|
||||
pool ! NumberPoolActor.ReturnNumber(number, Some(id))
|
||||
case None =>
|
||||
//do not log; use callback
|
||||
requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was de-registered"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object unregistration process.
|
||||
* If there is a successful request object to be found, continue the registration request.
|
||||
* @param request the original request data
|
||||
* @param number the number that was drawn from the `NumberPool`
|
||||
*/
|
||||
private def UnregistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = {
|
||||
request match {
|
||||
case Some(entry) =>
|
||||
processUnregisterResult(entry, number)
|
||||
case None =>
|
||||
log.error(s"returned a number but the rest of the request is missing (id:$id)")
|
||||
if(id != Long.MinValue) { //check to ignore endless loop of error-catching
|
||||
log.error("recovering the number from pool")
|
||||
NoCallbackGetSpecificNumber(number) //recovery?
|
||||
//no callback is possible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step of the object unregistration process.
|
||||
* This step completes revoking of the object's registration by consulting the `NumberSource`.
|
||||
* @param entry the original request data
|
||||
* @param number the number to use
|
||||
*/
|
||||
private def processUnregisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = {
|
||||
val obj = entry.target
|
||||
guid.latterPartUnregister(number) match {
|
||||
case Some(_) =>
|
||||
obj.Invalidate()
|
||||
entry.replyTo ! Success(obj)
|
||||
case None =>
|
||||
//do not log; use callback
|
||||
NoCallbackGetSpecificNumber(number, entry.targetPool) //recovery?
|
||||
entry.replyTo ! Failure(new Exception(s"failed to unregister a number; this may be a critical error"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a relevant logging message for an object that is trying to register is actually already registered.
|
||||
* @param obj the object that was trying to register
|
||||
* @param poolName the pool to which the object was trying to register
|
||||
*/
|
||||
private def AlreadyRegistered(obj : IdentifiableEntity, poolName : String) : Unit = {
|
||||
val msg =
|
||||
guid.WhichPool(obj) match {
|
||||
case Some(pname) =>
|
||||
if(poolName.equals(pname)) {
|
||||
s"to pool $poolName"
|
||||
}
|
||||
else {
|
||||
s"but to different pool $pname"
|
||||
}
|
||||
case None =>
|
||||
"but not to any pool known to this system"
|
||||
}
|
||||
log.warn(s"$obj already registered $msg")
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers.
|
||||
* @param number the number that was drawn from a `NumberPool`
|
||||
*/
|
||||
private def NoCallbackReturnNumber(number : Int) : Unit = {
|
||||
guid.WhichPool(number) match {
|
||||
case Some(pname) =>
|
||||
NoCallbackReturnNumber(number, pname)
|
||||
case None =>
|
||||
log.error(s"critical: tried to return number $number but could not find containing pool")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers.
|
||||
* To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine.
|
||||
* @param number the number that was drawn from a `NumberPool`
|
||||
* @param poolName the `NumberPool` from which the `number` was drawn
|
||||
* @see `UniqueNumberSystem.UnregistrationProcess(Option[GUIDRequest], Int, Int)`
|
||||
*/
|
||||
private def NoCallbackReturnNumber(number : Int, poolName : String) : Unit = {
|
||||
poolActors.get(poolName) match {
|
||||
case Some(pool) =>
|
||||
pool ! NumberPoolActor.ReturnNumber(number, Some(Long.MinValue))
|
||||
case None =>
|
||||
log.error(s"critical: tried to return number $number but did not find pool $poolName")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers.
|
||||
* @param number the number to be drawn from a `NumberPool`
|
||||
*/
|
||||
private def NoCallbackGetSpecificNumber(number : Int) : Unit = {
|
||||
guid.WhichPool(number) match {
|
||||
case Some(pname) =>
|
||||
NoCallbackGetSpecificNumber(number, pname)
|
||||
case None =>
|
||||
log.error(s"critical: tried to re-register number $number but could not find containing pool")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers.
|
||||
* To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine.
|
||||
* @param number the number to be drawn from a `NumberPool`
|
||||
* @param poolName the `NumberPool` from which the `number` is to be drawn
|
||||
* @see `UniqueNumberSystem.RegistrationProcess(Option[GUIDRequest], Int, Int)`
|
||||
*/
|
||||
private def NoCallbackGetSpecificNumber(number : Int, poolName : String) : Unit = {
|
||||
poolActors.get(poolName) match {
|
||||
case Some(pool) =>
|
||||
pool ! NumberPoolActor.GetSpecificNumber(number, Some(Long.MinValue))
|
||||
case None =>
|
||||
log.error(s"critical: tried to re-register number $number but did not find pool $poolName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UniqueNumberSystem {
|
||||
/**
|
||||
* Persistent record of the important information between the time fo request and the time of reply.
|
||||
* @param target the object
|
||||
* @param targetPool the name of the `NumberPool` being used
|
||||
* @param replyTo the callback `ActorRef`
|
||||
*/
|
||||
private final case class GUIDRequest(target : IdentifiableEntity, targetPool : String, replyTo : ActorRef)
|
||||
|
||||
/**
|
||||
* Transform `NumberPool`s into `NumberPoolActor`s and pair them with their name.
|
||||
* @param poolSource where the raw `NumberPools` are located
|
||||
* @param context used to create the `NumberPoolActor` instances
|
||||
* @return a `Map` of the pool names to the `ActorRef` created from the `NumberPool`
|
||||
*/
|
||||
def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit context : ActorContext) : Map[String, ActorRef] = {
|
||||
poolSource.Pools.map({ case ((pname, pool)) =>
|
||||
pname -> context.actorOf(Props(classOf[NumberPoolActor], pool), pname)
|
||||
}).toMap
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
|
||||
/**
|
||||
* A message for when an object has failed to be unregistered for some reason.
|
||||
* @param obj the object
|
||||
* @param ex the reason that the registration process failed
|
||||
*/
|
||||
final case class UnregisterFailure(obj : IdentifiableEntity, ex : Throwable)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.actor
|
||||
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
|
||||
/**
|
||||
* A message for when an object has been unregistered.
|
||||
* @param obj the object
|
||||
*/
|
||||
final case class UnregisterSuccess(obj : IdentifiableEntity)
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.misc
|
||||
|
||||
/**
|
||||
* This class is just a proof of concept model of a self-contained system.
|
||||
*/
|
||||
class AscendingNumberSource {
|
||||
val pool : Array[Int] = Array.ofDim[Int](65536)
|
||||
(0 to 65535).foreach(x => { pool(x) = x })
|
||||
var head : Int = 0
|
||||
|
||||
def Get() : Int = {
|
||||
val start : Int = head
|
||||
if(pool(head) == -1) {
|
||||
do {
|
||||
head = (head + 1) % pool.length
|
||||
}
|
||||
while(pool(head) == -1 && head != start)
|
||||
}
|
||||
if(head == start) {
|
||||
import net.psforever.objects.entity.NoGUIDException
|
||||
throw NoGUIDException("no unused numbers available")
|
||||
}
|
||||
val outNumber : Int = head
|
||||
pool(head) = -1
|
||||
outNumber
|
||||
}
|
||||
|
||||
def Return(number : Int) : Unit = {
|
||||
pool(number) = number
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.misc
|
||||
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable}
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Accept a task in waiting and series of lesser tasks that complete the provided primary task.
|
||||
* Receive periodic updates on the states of the lesser tasks and, when these sub-tasks have been accomplished,
|
||||
* declare the primary task accomplished as well.<br>
|
||||
* <br>
|
||||
* This ia admittedly a simplistic model of task resolution, currently, and is rather specific and limited.
|
||||
* Generalizing and expanding on this class in the future might be beneficial.
|
||||
* @param obj the primary task
|
||||
* @param list a series of sub-tasks that need to be completed before the pimrary task can be completed
|
||||
* @param callback where to report about the pirmary task having succeeded or failed
|
||||
* @param timeoutDuration a delay during which sub-tasks are permitted to be accomplished;
|
||||
* after this grave period is over, the task has failed
|
||||
*/
|
||||
class RegistrationTaskResolver[T <: IdentifiableEntity](private val obj : T, private val list : List[T], callback : ActorRef, timeoutDuration : FiniteDuration) extends Actor {
|
||||
/** sub-tasks that contribute to completion of the task */
|
||||
private val checklist : Array[Boolean] = Array.fill[Boolean](list.length)(false)
|
||||
/** whether or not it matters that sub-tasks are coming in */
|
||||
private var valid : Boolean = true
|
||||
/** declares when the task has taken too long to complete */
|
||||
private val taskTimeout : Cancellable = context.system.scheduler.scheduleOnce(timeoutDuration, self, Failure(new TimeoutException(s"a task for $obj has timed out")))
|
||||
private[this] val log = org.log4s.getLogger
|
||||
ConfirmTask(Success(true)) //check for auto-completion
|
||||
|
||||
def receive : Receive = {
|
||||
case Success(objn)=>
|
||||
ConfirmTask(ConfirmSubtask(objn.asInstanceOf[T]))
|
||||
|
||||
case Failure(ex)=>
|
||||
FailedTask(ex)
|
||||
|
||||
case msg =>
|
||||
log.warn(s"unexpected message received - ${msg.toString}")
|
||||
}
|
||||
|
||||
/**
|
||||
* If this object is still accepting task resolutions, determine if that sub-task can be checked off.
|
||||
* @param objn the sub-task entry
|
||||
* @return a successful pass or a failure if the task can't be found;
|
||||
* a "successful failure" if task resolutions are no longer accepted
|
||||
*/
|
||||
private def ConfirmSubtask(objn : T) : Try[Boolean] = {
|
||||
if(valid) {
|
||||
if(MatchSubtask(objn, list.iterator)) {
|
||||
Success(true)
|
||||
}
|
||||
else {
|
||||
Failure(new Exception(s"can not find a subtask to check off - ${objn.toString}"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Success(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a sub-task from a `List` of sub-tasks and mark it as completed, if found.
|
||||
* @param objn the sub-task entry
|
||||
* @param iter_list an `Iterator` to the list of sub-tasks
|
||||
* @param index the index of this entry;
|
||||
* defaults to zero
|
||||
* @return whether or not the subtask has been marked as completed
|
||||
*/
|
||||
@tailrec private def MatchSubtask(objn : T, iter_list : Iterator[T], index : Int = 0) : Boolean = {
|
||||
if(!iter_list.hasNext) {
|
||||
false
|
||||
}
|
||||
else {
|
||||
val subtask = iter_list.next
|
||||
if(subtask.equals(objn)) {
|
||||
checklist(index) = true
|
||||
true
|
||||
}
|
||||
else {
|
||||
MatchSubtask(objn, iter_list, index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether all sub-tasks have been completed successfully.
|
||||
* If so, complete the primary task.
|
||||
* @param subtaskComplete the status of the recent sub-task confirmation that triggered this confirmation request
|
||||
*/
|
||||
private def ConfirmTask(subtaskComplete : Try[Boolean]) : Unit = {
|
||||
if(valid) {
|
||||
subtaskComplete match {
|
||||
case Success(true) =>
|
||||
if(!checklist.contains(false)) {
|
||||
FulfillTask()
|
||||
}
|
||||
case Success(false) =>
|
||||
log.warn(s"when checking a task for ${obj.toString}, arrived at a state where we previously failed a subtask but main task still valid")
|
||||
case Failure(ex) =>
|
||||
FailedTask(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All sub-tasks have been completed; the main task can also be completed.
|
||||
* Alert interested parties that the task is performed successfully.
|
||||
* Stop as soon as possible.
|
||||
*/
|
||||
private def FulfillTask() : Unit = {
|
||||
valid = false
|
||||
callback ! Success(obj)
|
||||
taskTimeout.cancel()
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
/**
|
||||
* The main task can not be completed.
|
||||
* Clean up as much as possible and alert interested parties that the task has been dropped.
|
||||
* Let this `Actor` stop gracefully.
|
||||
* @param ex why the main task can not be completed
|
||||
*/
|
||||
private def FailedTask(ex : Throwable) : Unit = {
|
||||
valid = false
|
||||
callback ! Failure(ex)
|
||||
taskTimeout.cancel()
|
||||
import akka.pattern.gracefulStop
|
||||
gracefulStop(self, 2 seconds) //give time for any other messages; avoid dead letters
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.guid.source
|
||||
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
|
||||
import net.psforever.objects.guid.AvailabilityPolicy
|
||||
|
||||
/**
|
||||
* A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
|
||||
* The numbers are considered to be exclusive.<br>
|
||||
* <br>
|
||||
* This source utilizes all positive integers (to `Int.MaxValue`, anyway) and zero.
|
||||
* It allocates number `Monitors` as it needs them.
|
||||
* While this allows for a wide range of possible numbers, the internal structure expands and contracts as needed.
|
||||
* The underlying flexible structure is a `LongMap` and is subject to constraints regarding `LongMap` growth.
|
||||
*/
|
||||
class MaxNumberSource() extends NumberSource {
|
||||
import scala.collection.mutable
|
||||
private val hash : mutable.LongMap[Key] = mutable.LongMap[Key]() //TODO consider seeding an initialBufferSize
|
||||
private var allowRestrictions : Boolean = true
|
||||
|
||||
def Size : Int = Int.MaxValue
|
||||
|
||||
def CountAvailable : Int = Size - CountUsed
|
||||
|
||||
def CountUsed : Int = hash.size
|
||||
|
||||
override def Test(guid : Int) : Boolean = guid > -1
|
||||
|
||||
def Get(number : Int) : Option[SecureKey] = {
|
||||
if(!Test(number)) {
|
||||
None
|
||||
}
|
||||
else {
|
||||
val existing : Option[Key] = hash.get(number).orElse({
|
||||
val key : Key = new Key
|
||||
key.Policy = AvailabilityPolicy.Available
|
||||
hash.put(number, key)
|
||||
Some(key)
|
||||
})
|
||||
Some(new SecureKey(number, existing.get))
|
||||
}
|
||||
}
|
||||
|
||||
// def GetAll(list : List[Int]) : List[SecureKey] = {
|
||||
// list.map(number =>
|
||||
// hash.get(number) match {
|
||||
// case Some(key) =>
|
||||
// new SecureKey(number, key)
|
||||
// case _ =>
|
||||
// new SecureKey(number, new Key { Policy = AvailabilityPolicy.Available })
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// def GetAll( p : Key => Boolean ) : List[SecureKey] = {
|
||||
// hash.filter(entry => p.apply(entry._2)).map(entry => new SecureKey(entry._1.toInt, entry._2)).toList
|
||||
// }
|
||||
|
||||
def Available(number : Int) : Option[LoanedKey] = {
|
||||
if(!Test(number)) {
|
||||
throw new IndexOutOfBoundsException("number can not be negative")
|
||||
}
|
||||
hash.get(number) match {
|
||||
case Some(_) =>
|
||||
None
|
||||
case _ =>
|
||||
val key : Key = new Key
|
||||
key.Policy = AvailabilityPolicy.Leased
|
||||
hash.put(number, key)
|
||||
Some(new LoanedKey(number, key))
|
||||
}
|
||||
}
|
||||
|
||||
def Return(number : Int) : Option[IdentifiableEntity] = {
|
||||
val existing = hash.get(number)
|
||||
if(existing.isDefined && existing.get.Policy == AvailabilityPolicy.Leased) {
|
||||
hash -= number
|
||||
val obj = existing.get.Object
|
||||
existing.get.Object = None
|
||||
obj
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def Restrict(number : Int) : Option[LoanedKey] = {
|
||||
if(allowRestrictions) {
|
||||
val existing : Key = hash.get(number).orElse({
|
||||
val key : Key = new Key
|
||||
hash.put(number, key)
|
||||
Some(key)
|
||||
}).get
|
||||
existing.Policy = AvailabilityPolicy.Restricted
|
||||
Some(new LoanedKey(number, existing))
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def FinalizeRestrictions : List[Int] = {
|
||||
allowRestrictions = false
|
||||
hash.filter(entry => entry._2.Policy == AvailabilityPolicy.Restricted).map(entry => entry._1.toInt).toList
|
||||
}
|
||||
|
||||
def Clear() : List[IdentifiableEntity] = {
|
||||
val list : List[IdentifiableEntity] = hash.values.filter(key => key.Object.isDefined).map(key => key.Object.get).toList
|
||||
hash.clear()
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
object MaxNumberSource {
|
||||
def apply() : MaxNumberSource = {
|
||||
new MaxNumberSource()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject
|
||||
|
||||
import net.psforever.objects.Player
|
||||
|
||||
//temporary location for these messages
|
||||
object CommonMessages {
|
||||
final case class Hack(player : Player)
|
||||
final case class ClearHack()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
|
||||
/**
|
||||
* An object layered on top of the standard game object class that maintains an internal `ActorRef`.
|
||||
* A measure of synchronization can be managed using this `Actor`.
|
||||
*/
|
||||
abstract class PlanetSideServerObject extends PlanetSideGameObject {
|
||||
private var actor = ActorRef.noSender
|
||||
|
||||
/**
|
||||
* Retrieve a reference to the internal `Actor`.
|
||||
* @return the internal `ActorRef`
|
||||
*/
|
||||
def Actor : ActorRef = actor
|
||||
|
||||
/**
|
||||
* Assign an `Actor` to act for this server object.
|
||||
* This reference is only set once, that is, as long as the internal `ActorRef` directs to `Actor.noSender` (`null`).
|
||||
* @param control the `Actor` whose functionality will govern this server object
|
||||
* @return the current internal `ActorRef`
|
||||
*/
|
||||
def Actor_=(control : ActorRef) : ActorRef = {
|
||||
if(actor == ActorRef.noSender) {
|
||||
actor = control
|
||||
}
|
||||
actor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.builders
|
||||
|
||||
import akka.actor.Props
|
||||
import net.psforever.objects.serverobject.doors.{Door, DoorControl, DoorDefinition}
|
||||
|
||||
/**
|
||||
* Wrapper `Class` designed to instantiate a `Door` server object.
|
||||
* @param ddef a `DoorDefinition` object, indicating the specific functionality of the resulting `Door`
|
||||
* @param id the globally unique identifier to which this `Door` will be registered
|
||||
*/
|
||||
class DoorObjectBuilder(private val ddef : DoorDefinition, private val id : Int) extends ServerObjectBuilder[Door] {
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
||||
def Build(implicit context : ActorContext, guid : NumberPoolHub) : Door = {
|
||||
val obj = Door(ddef)
|
||||
guid.register(obj, id) //non-Actor GUID registration
|
||||
obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_${obj.GUID.guid}")
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
object DoorObjectBuilder {
|
||||
/**
|
||||
* Overloaded constructor for a `DoorObjectBuilder`.
|
||||
* @param ddef a `DoorDefinition` object
|
||||
* @param id a globally unique identifier
|
||||
* @return a `DoorObjectBuilder` object
|
||||
*/
|
||||
def apply(ddef : DoorDefinition, id : Int) : DoorObjectBuilder = {
|
||||
new DoorObjectBuilder(ddef, id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.builders
|
||||
|
||||
import akka.actor.Props
|
||||
import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition}
|
||||
|
||||
/**
|
||||
* Wrapper `Class` designed to instantiate a door lock server object that is sensitive to user faction affiliation.
|
||||
* @param idef a `IFFLockDefinition` object, indicating the specific functionality
|
||||
* @param id the globally unique identifier to which this `IFFLock` will be registered
|
||||
*/
|
||||
class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id : Int) extends ServerObjectBuilder[IFFLock] {
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
||||
def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = {
|
||||
val obj = IFFLock(idef)
|
||||
guid.register(obj, id) //non-Actor GUID registration
|
||||
obj.Actor = context.actorOf(Props(classOf[IFFLockControl], obj), s"${idef.Name}_${obj.GUID.guid}")
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
object IFFLockObjectBuilder {
|
||||
/**
|
||||
* Overloaded constructor for a `IFFLockObjectBuilder`.
|
||||
* @param idef an `IFFLock` object
|
||||
* @param id a globally unique identifier
|
||||
* @return an `IFFLockObjectBuilder` object
|
||||
*/
|
||||
def apply(idef : IFFLockDefinition, id : Int) : IFFLockObjectBuilder = {
|
||||
new IFFLockObjectBuilder(idef, id)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.zones
|
||||
package net.psforever.objects.serverobject.builders
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
|
|
@ -7,9 +7,11 @@ import net.psforever.objects.guid.NumberPoolHub
|
|||
|
||||
/**
|
||||
* Wrapper `Trait` designed to be extended to implement custom object instantiation logic at the `ZoneMap` level.
|
||||
* @tparam A any object that extends from PlanetSideGameObject
|
||||
* @see `Zone.Init`
|
||||
*/
|
||||
trait ServerObjectBuilder {
|
||||
//TODO can we changed PlanetSideGameObject -> PlanetSideServerObject?
|
||||
trait ServerObjectBuilder[A <: PlanetSideGameObject] {
|
||||
/**
|
||||
* Instantiate and configure the given server object
|
||||
* (at a later time compared to the construction of the builder class).<br>
|
||||
|
|
@ -23,5 +25,5 @@ trait ServerObjectBuilder {
|
|||
* defaults to `null`
|
||||
* @return the object that was created and integrated into the `Zone`
|
||||
*/
|
||||
def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : PlanetSideGameObject
|
||||
def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : A
|
||||
}
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.zones
|
||||
package net.psforever.objects.serverobject.builders
|
||||
|
||||
import net.psforever.objects.terminals.{Terminal, TerminalDefinition}
|
||||
import akka.actor.Props
|
||||
import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition}
|
||||
|
||||
/**
|
||||
* Wrapper `Class` designed to instantiate a `Terminal` server object.
|
||||
* @param tdef a `TerminalDefinition` object, indicating the specific functionality of the resulting `Terminal`
|
||||
* @param id the globally unique identifier to which this `Terminal` will be registered
|
||||
*/
|
||||
class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder {
|
||||
class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder[Terminal] {
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
||||
def Build(implicit context : ActorContext, guid : NumberPoolHub) : Terminal = {
|
||||
val obj = Terminal(tdef)
|
||||
guid.register(obj, id) //non-Actor GUID registration
|
||||
obj.Actor //it's necessary to register beforehand because the Actor name utilizes the GUID
|
||||
obj.Actor = context.actorOf(Props(classOf[TerminalControl], obj), s"${tdef.Name}_${obj.GUID.guid}")
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.doors
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
/**
|
||||
* A temporary class to represent "facilities" and "structures."
|
||||
* @param id the map id of the base
|
||||
*/
|
||||
class Base(private val id : Int) {
|
||||
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
|
||||
def Id : Int = id
|
||||
|
||||
def Faction : PlanetSideEmpire.Value = faction
|
||||
|
||||
def Faction_=(emp : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = {
|
||||
faction = emp
|
||||
Faction
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.doors
|
||||
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.packet.game.UseItemMessage
|
||||
|
||||
/**
|
||||
* A structure-owned server object that is a "door" that can open and can close.
|
||||
* @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
class Door(private val ddef : DoorDefinition) extends PlanetSideServerObject {
|
||||
private var openState : Boolean = false
|
||||
private var lockState : Boolean = false
|
||||
|
||||
def Open : Boolean = openState
|
||||
|
||||
def Open_=(open : Boolean) : Boolean = {
|
||||
openState = open
|
||||
Open
|
||||
}
|
||||
|
||||
def Locked : Boolean = lockState
|
||||
|
||||
def Locked_=(lock : Boolean) : Boolean = {
|
||||
lockState = lock
|
||||
Locked
|
||||
}
|
||||
|
||||
def Use(player : Player, msg : UseItemMessage) : Door.Exchange = {
|
||||
if(!lockState && !openState) {
|
||||
openState = true
|
||||
Door.OpenEvent()
|
||||
}
|
||||
else if(openState) {
|
||||
openState = false
|
||||
Door.CloseEvent()
|
||||
}
|
||||
else {
|
||||
Door.NoEvent()
|
||||
}
|
||||
}
|
||||
|
||||
def Definition : DoorDefinition = ddef
|
||||
}
|
||||
|
||||
object Door {
|
||||
/**
|
||||
* Entry message into this `Door` that carries the request.
|
||||
* @param player the player who sent this request message
|
||||
* @param msg the original packet carrying the request
|
||||
*/
|
||||
final case class Use(player : Player, msg : UseItemMessage)
|
||||
|
||||
/**
|
||||
* A basic `Trait` connecting all of the actionable `Door` response messages.
|
||||
*/
|
||||
sealed trait Exchange
|
||||
|
||||
/**
|
||||
* Message that carries the result of the processed request message back to the original user (`player`).
|
||||
* @param player the player who sent this request message
|
||||
* @param msg the original packet carrying the request
|
||||
* @param response the result of the processed request
|
||||
*/
|
||||
final case class DoorMessage(player : Player, msg : UseItemMessage, response : Exchange)
|
||||
|
||||
/**
|
||||
* This door will open.
|
||||
*/
|
||||
final case class OpenEvent() extends Exchange
|
||||
|
||||
/**
|
||||
* This door will close.
|
||||
*/
|
||||
final case class CloseEvent() extends Exchange
|
||||
|
||||
/**
|
||||
* This door will do nothing.
|
||||
*/
|
||||
final case class NoEvent() extends Exchange
|
||||
|
||||
/**
|
||||
* Overloaded constructor.
|
||||
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
def apply(tdef : DoorDefinition) : Door = {
|
||||
new Door(tdef)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.doors
|
||||
|
||||
import akka.actor.Actor
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific `Door`.
|
||||
* @param door the `Door` object being governed
|
||||
*/
|
||||
class DoorControl(door : Door) extends Actor {
|
||||
def receive : Receive = {
|
||||
case Door.Use(player, msg) =>
|
||||
sender ! Door.DoorMessage(player, msg, door.Use(player, msg))
|
||||
|
||||
case _ =>
|
||||
sender ! Door.NoEvent()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.doors
|
||||
|
||||
import net.psforever.objects.definition.ObjectDefinition
|
||||
|
||||
/**
|
||||
* The definition for any `Door`.
|
||||
* Object Id 242 is a generic door.
|
||||
*/
|
||||
class DoorDefinition extends ObjectDefinition(242) {
|
||||
Name = "door"
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.locks
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* A structure-owned server object that is a "door lock."<br>
|
||||
* <br>
|
||||
* The "door lock" exerts an "identify friend or foe" field that detects the faction affiliation of a target player.
|
||||
* It also indirectly inherits faction affiliation from the structure to which it is connected
|
||||
* or it can be "hacked" whereupon the person exploiting it leaves their "faction" as the aforementioned affiliated faction.
|
||||
* The `IFFLock` is ideally associated with a server map object - a `Door` - to which it acts as a gatekeeper.
|
||||
* @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
class IFFLock(private val idef : IFFLockDefinition) extends PlanetSideServerObject {
|
||||
/**
|
||||
* An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
|
||||
*/
|
||||
private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
|
||||
|
||||
def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
|
||||
|
||||
def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
|
||||
|
||||
/**
|
||||
* Set the hack state of this object by recording important information about the player that caused it.
|
||||
* Set the hack state if there is no current hack state.
|
||||
* Override the hack state with a new hack state if the new user has different faction affiliation.
|
||||
* @param agent a `Player`, or no player
|
||||
* @return the player hack entry
|
||||
*/
|
||||
def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
|
||||
hackedBy match {
|
||||
case None =>
|
||||
//set the hack state if there is no current hack state
|
||||
if(agent.isDefined) {
|
||||
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
|
||||
}
|
||||
case Some(_) =>
|
||||
//clear the hack state
|
||||
if(agent.isEmpty) {
|
||||
hackedBy = None
|
||||
}
|
||||
//override the hack state with a new hack state if the new user has different faction affiliation
|
||||
else if(agent.get.Faction != hackedBy.get._1.Faction) {
|
||||
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
|
||||
}
|
||||
}
|
||||
HackedBy
|
||||
}
|
||||
|
||||
def Definition : IFFLockDefinition = idef
|
||||
}
|
||||
|
||||
object IFFLock {
|
||||
/**
|
||||
* Overloaded constructor.
|
||||
* @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
def apply(idef : IFFLockDefinition) : IFFLock = {
|
||||
new IFFLock(idef)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.locks
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific `IFFLock`.
|
||||
* @param lock the `IFFLock` object being governed
|
||||
* @see `CommonMessages`
|
||||
*/
|
||||
class IFFLockControl(lock : IFFLock) extends Actor {
|
||||
def receive : Receive = {
|
||||
case CommonMessages.Hack(player) =>
|
||||
lock.HackedBy = player
|
||||
|
||||
case CommonMessages.ClearHack() =>
|
||||
lock.HackedBy = None
|
||||
|
||||
case _ => ; //no default message
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.locks
|
||||
|
||||
import net.psforever.objects.definition.ObjectDefinition
|
||||
|
||||
/**
|
||||
* The definition for any `IFFLock`.
|
||||
* Object Id 451 is a generic external lock.
|
||||
*/
|
||||
class IFFLockDefinition extends ObjectDefinition(451) {
|
||||
Name = "iff_lock"
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.packet.game.ItemTransactionMessage
|
||||
import net.psforever.types.CertificationType
|
||||
|
||||
/**
|
||||
* The definition for any `Terminal` that is of a type "cert_terminal" (certification terminal).
|
||||
* `Learn` and `Sell` `CertificationType` entries, gaining access to different `Equipment` and `Vehicles`.
|
||||
*/
|
||||
class CertTerminalDefinition extends TerminalDefinition(171) {
|
||||
Name = "cert_terminal"
|
||||
|
||||
/**
|
||||
* The certifications available.
|
||||
* All entries are listed on page (tab) number 0.
|
||||
*/
|
||||
private val certificationList : Map[String, (CertificationType.Value, Int)] = Map(
|
||||
"medium_assault" -> (CertificationType.MediumAssault, 2),
|
||||
"reinforced_armo" -> (CertificationType.ReinforcedExoSuit, 3),
|
||||
"quad_all" -> (CertificationType.ATV, 1),
|
||||
"switchblade" -> (CertificationType.Switchblade, 1),
|
||||
"harasser" -> (CertificationType.Harasser, 1),
|
||||
"anti_vehicular" -> (CertificationType.AntiVehicular, 3),
|
||||
"heavy_assault" -> (CertificationType.HeavyAssault, 4),
|
||||
"sniper" -> (CertificationType.Sniping, 3),
|
||||
"special_assault" -> (CertificationType.SpecialAssault, 3),
|
||||
"special_assault_2" -> (CertificationType.EliteAssault, 1),
|
||||
"infiltration_suit" -> (CertificationType.InfiltrationSuit, 2),
|
||||
"max_anti_personnel" -> (CertificationType.AIMAX, 3),
|
||||
"max_anti_vehicular" -> (CertificationType.AVMAX, 3),
|
||||
"max_anti_aircraft" -> (CertificationType.AAMAX, 2),
|
||||
"max_all" -> (CertificationType.UniMAX, 6),
|
||||
"air_cavalry_scout" -> (CertificationType.AirCavalryScout, 3),
|
||||
"air_cavalry_assault" -> (CertificationType.AirCavalryAssault, 2),
|
||||
"air_cavalry_interceptor" -> (CertificationType.AirCavalryInterceptor, 2),
|
||||
"air_support" -> (CertificationType.AirSupport, 3),
|
||||
"gunship" -> (CertificationType.GalaxyGunship, 2),
|
||||
"phantasm" -> (CertificationType.Phantasm, 3),
|
||||
"armored_assault1" -> (CertificationType.ArmoredAssault1, 2),
|
||||
"armored_assault2" -> (CertificationType.ArmoredAssault2, 1),
|
||||
"flail" -> (CertificationType.Flail, 1),
|
||||
"assault_buggy" -> (CertificationType.AssaultBuggy, 3),
|
||||
"ground_support" -> (CertificationType.GroundSupport, 2),
|
||||
"ground_transport" -> (CertificationType.GroundTransport, 2),
|
||||
"light_scout" -> (CertificationType.LightScout, 5),
|
||||
"Repair" -> (CertificationType.Engineering, 3),
|
||||
"combat_engineering" -> (CertificationType.CombatEngineering, 2),
|
||||
"ce_offense" -> (CertificationType.AssaultEngineering, 3),
|
||||
"ce_defense" -> (CertificationType.FortificationEngineering, 3),
|
||||
"ce_advanced" -> (CertificationType.AdvancedEngineering, 5),
|
||||
"Hacking" -> (CertificationType.Hacking, 3),
|
||||
"advanced_hacking" -> (CertificationType.AdvancedHacking, 2),
|
||||
"expert_hacking" -> (CertificationType.ExpertHacking, 2),
|
||||
"virus_hacking" -> (CertificationType.DataCorruption, 3),
|
||||
"electronics_expert" -> (CertificationType.ElectronicsExpert, 4),
|
||||
"Medical" -> (CertificationType.Medical, 3),
|
||||
"advanced_medical" -> (CertificationType.AdvancedMedical, 2)
|
||||
//TODO bfr certification entries
|
||||
)
|
||||
|
||||
/**
|
||||
* Process a `TransactionType.Learn` action by the user.
|
||||
* @param player the player
|
||||
* @param msg the original packet carrying the request
|
||||
* @return an actionable message that explains how to process the request
|
||||
*/
|
||||
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { //Learn
|
||||
certificationList.get(msg.item_name) match {
|
||||
case Some((cert, cost)) =>
|
||||
Terminal.LearnCertification(cert, cost)
|
||||
case None =>
|
||||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a `TransactionType.Sell` action by the user.
|
||||
* @param player the player
|
||||
* @param msg the original packet carrying the request
|
||||
* @return an actionable message that explains how to process the request
|
||||
*/
|
||||
def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
|
||||
certificationList.get(msg.item_name) match {
|
||||
case Some((cert, cost)) =>
|
||||
Terminal.SellCertification(cert, cost)
|
||||
case None =>
|
||||
Terminal.NoDeal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action is not supported by this type of `Terminal`.
|
||||
* @param player the player
|
||||
* @param msg the original packet carrying the request
|
||||
* @return `Terminal.NoEvent` always
|
||||
*/
|
||||
def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal()
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.terminals
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import net.psforever.objects.InfantryLoadout.Simplification
|
||||
import net.psforever.objects.{Player, Tool}
|
||||
|
|
@ -10,6 +10,11 @@ import net.psforever.packet.game.ItemTransactionMessage
|
|||
|
||||
import scala.annotation.switch
|
||||
|
||||
/**
|
||||
* The definition for any `Terminal` that is of a type "order_terminal".
|
||||
* `Buy` and `Sell` `Equipment` items and `AmmoBox` items.
|
||||
* Change `ExoSuitType` and retrieve `InfantryLoadout` entries.
|
||||
*/
|
||||
class OrderTerminalDefinition extends TerminalDefinition(612) {
|
||||
Name = "order_terminal"
|
||||
|
||||
|
|
@ -21,10 +26,10 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
|
|||
|
||||
/**
|
||||
* Process a `TransactionType.Buy` action by the user.
|
||||
* Either attempt to purchase equipment or attempt to switch directly to a different exo-suit.
|
||||
* @param player the player
|
||||
* @param msg the original packet carrying the request
|
||||
* @return an actionable message that explains how to process the request;
|
||||
* either you attempt to purchase equipment or attempt to switch directly to a different exo-suit
|
||||
* @return an actionable message that explains how to process the request
|
||||
*/
|
||||
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
|
||||
(msg.item_page : @switch) match {
|
||||
|
|
@ -64,6 +69,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
|
|||
/**
|
||||
* Process a `TransactionType.Sell` action by the user.
|
||||
* There is no specific `order_terminal` tab associated with this action.
|
||||
* Additionally, the equipment to be sold ia almost always in the player's `FreeHand` slot.
|
||||
* Selling `Equipment` is always permitted.
|
||||
* @param player the player
|
||||
* @param msg the original packet carrying the request
|
||||
|
|
@ -81,7 +87,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
|
|||
* @param msg the original packet carrying the request
|
||||
* @return an actionable message that explains how to process the request
|
||||
*/
|
||||
def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
|
||||
def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
|
||||
if(msg.item_page == 4) { //Favorites tab
|
||||
player.LoadLoadout(msg.unk1) match {
|
||||
case Some(loadout) =>
|
||||
|
|
@ -1,55 +1,59 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.terminals
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player}
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.packet.game.ItemTransactionMessage
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID}
|
||||
import net.psforever.types.{ExoSuitType, TransactionType, Vector3}
|
||||
|
||||
/**
|
||||
* na
|
||||
* A structure-owned server object that is a "terminal" that can be accessed for amenities and services.
|
||||
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject {
|
||||
/** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */
|
||||
private var actor = ActorRef.noSender
|
||||
class Terminal(tdef : TerminalDefinition) extends PlanetSideServerObject {
|
||||
/**
|
||||
* An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
|
||||
*/
|
||||
private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
|
||||
|
||||
def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
|
||||
|
||||
def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
|
||||
|
||||
/**
|
||||
* Get access to the internal `TerminalControl` `Actor` for this `Terminal`.
|
||||
* If called for the first time, create the said `Actor`.
|
||||
* Must be called only after the globally unique identifier has been set.
|
||||
* @param context the `ActorContext` under which this `Terminal`'s `Actor` will be created
|
||||
* @return the `Terminal`'s `Actor`
|
||||
* Set the hack state of this object by recording important information about the player that caused it.
|
||||
* Set the hack state if there is no current hack state.
|
||||
* Override the hack state with a new hack state if the new user has different faction affiliation.
|
||||
* @param agent a `Player`, or no player
|
||||
* @return the player hack entry
|
||||
*/
|
||||
def Actor(implicit context : ActorContext) : ActorRef = {
|
||||
if(actor == ActorRef.noSender) {
|
||||
actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}")
|
||||
def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
|
||||
hackedBy match {
|
||||
case None =>
|
||||
//set the hack state if there is no current hack state
|
||||
if(agent.isDefined) {
|
||||
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
|
||||
}
|
||||
case Some(_) =>
|
||||
//clear the hack state
|
||||
if(agent.isEmpty) {
|
||||
hackedBy = None
|
||||
}
|
||||
//override the hack state with a new hack state if the new user has different faction affiliation
|
||||
else if(agent.get.Faction != hackedBy.get._1.Faction) {
|
||||
hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
|
||||
}
|
||||
}
|
||||
actor
|
||||
HackedBy
|
||||
}
|
||||
|
||||
//the following fields and related methods are neither finalized no integrated; GOTO Request
|
||||
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var hackedBy : Option[PlanetSideEmpire.Value] = None
|
||||
//the following fields and related methods are neither finalized nor integrated; GOTO Request
|
||||
private var health : Int = 100 //TODO not real health value
|
||||
|
||||
def Faction : PlanetSideEmpire.Value = faction
|
||||
|
||||
def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy
|
||||
|
||||
def Health : Int = health
|
||||
|
||||
def Convert(toFaction : PlanetSideEmpire.Value) : Unit = {
|
||||
hackedBy = None
|
||||
faction = toFaction
|
||||
}
|
||||
|
||||
def HackedBy(toFaction : Option[PlanetSideEmpire.Value]) : Unit = {
|
||||
hackedBy = if(toFaction.contains(faction)) { None } else { toFaction }
|
||||
}
|
||||
|
||||
def Damaged(dam : Int) : Unit = {
|
||||
health = Math.max(0, Health - dam)
|
||||
}
|
||||
|
|
@ -66,14 +70,14 @@ class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject {
|
|||
*/
|
||||
def Request(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
|
||||
msg.transaction_type match {
|
||||
case TransactionType.Buy =>
|
||||
case TransactionType.Buy | TransactionType.Learn =>
|
||||
tdef.Buy(player, msg)
|
||||
|
||||
case TransactionType.Sell =>
|
||||
tdef.Sell(player, msg)
|
||||
|
||||
case TransactionType.InfantryLoadout =>
|
||||
tdef.InfantryLoadout(player, msg)
|
||||
tdef.Loadout(player, msg)
|
||||
|
||||
case _ =>
|
||||
Terminal.NoDeal()
|
||||
|
|
@ -132,6 +136,12 @@ object Terminal {
|
|||
*/
|
||||
//TODO if there are exceptions, find them
|
||||
final case class SellEquipment() extends Exchange
|
||||
|
||||
import net.psforever.types.CertificationType
|
||||
final case class LearnCertification(cert : CertificationType.Value, cost : Int) extends Exchange
|
||||
|
||||
final case class SellCertification(cert : CertificationType.Value, cost : Int) extends Exchange
|
||||
|
||||
/**
|
||||
* Recover a former exo-suit and `Equipment` configuration that the `Player` possessed.
|
||||
* A result of a processed request.
|
||||
|
|
@ -142,14 +152,11 @@ object Terminal {
|
|||
*/
|
||||
final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange
|
||||
|
||||
/**
|
||||
* Overloaded constructor.
|
||||
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
|
||||
*/
|
||||
def apply(tdef : TerminalDefinition) : Terminal = {
|
||||
new Terminal(tdef)
|
||||
}
|
||||
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = {
|
||||
val obj = new Terminal(tdef)
|
||||
obj.GUID = guid
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.terminals
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import akka.actor.Actor
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific `Terminal`.<br>
|
||||
* <br>
|
||||
* For now, the only important message being managed is `Terminal.Request`.
|
||||
* An `Actor` that handles messages being dispatched to a specific `Terminal`.
|
||||
* @param term the `Terminal` object being governed
|
||||
*/
|
||||
class TerminalControl(term : Terminal) extends Actor {
|
||||
|
|
@ -14,18 +12,6 @@ class TerminalControl(term : Terminal) extends Actor {
|
|||
case Terminal.Request(player, msg) =>
|
||||
sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg))
|
||||
|
||||
case TemporaryTerminalMessages.Convert(fact) =>
|
||||
term.Convert(fact)
|
||||
|
||||
case TemporaryTerminalMessages.Hacked(fact) =>
|
||||
term.HackedBy(fact)
|
||||
|
||||
case TemporaryTerminalMessages.Damaged(dam) =>
|
||||
term.Damaged(dam)
|
||||
|
||||
case TemporaryTerminalMessages.Repaired(rep) =>
|
||||
term.Repair(rep)
|
||||
|
||||
case _ =>
|
||||
sender ! Terminal.NoDeal()
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.terminals
|
||||
package net.psforever.objects.serverobject.terminals
|
||||
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.definition._
|
||||
|
|
@ -7,8 +7,6 @@ import net.psforever.objects.equipment.Equipment
|
|||
import net.psforever.packet.game.ItemTransactionMessage
|
||||
import net.psforever.types.ExoSuitType
|
||||
|
||||
import scala.collection.immutable.HashMap
|
||||
|
||||
/**
|
||||
* The definition for any `Terminal`.
|
||||
* @param objectId the object's identifier number
|
||||
|
|
@ -17,7 +15,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
Name = "terminal"
|
||||
|
||||
/**
|
||||
* The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` activity.
|
||||
* The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` and `TransactionType.Learn` activity.
|
||||
*/
|
||||
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
|
||||
|
||||
|
|
@ -29,7 +27,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
/**
|
||||
* The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity.
|
||||
*/
|
||||
def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
|
||||
def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
|
||||
|
||||
/**
|
||||
* A `Map` of information for changing exo-suits.
|
||||
|
|
@ -49,7 +47,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
* key - an identification string sent by the client
|
||||
* value - a curried function that builds the object
|
||||
*/
|
||||
protected val infantryAmmunition : HashMap[String, ()=>Equipment] = HashMap(
|
||||
protected val infantryAmmunition : Map[String, ()=>Equipment] = Map(
|
||||
"9mmbullet" -> MakeAmmoBox(bullet_9mm),
|
||||
"9mmbullet_AP" -> MakeAmmoBox(bullet_9mm_AP),
|
||||
"shotgun_shell" -> MakeAmmoBox(shotgun_shell),
|
||||
|
|
@ -75,7 +73,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
* key - an identification string sent by the client
|
||||
* value - a curried function that builds the object
|
||||
*/
|
||||
protected val supportAmmunition : HashMap[String, ()=>Equipment] = HashMap(
|
||||
protected val supportAmmunition : Map[String, ()=>Equipment] = Map(
|
||||
"health_canister" -> MakeAmmoBox(health_canister),
|
||||
"armor_canister" -> MakeAmmoBox(armor_canister),
|
||||
"upgrade_canister" -> MakeAmmoBox(upgrade_canister)
|
||||
|
|
@ -86,7 +84,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
* key - an identification string sent by the client
|
||||
* value - a curried function that builds the object
|
||||
*/
|
||||
protected val vehicleAmmunition : HashMap[String, ()=>Equipment] = HashMap(
|
||||
protected val vehicleAmmunition : Map[String, ()=>Equipment] = Map(
|
||||
"35mmbullet" -> MakeAmmoBox(bullet_35mm),
|
||||
"hellfire_ammo" -> MakeAmmoBox(hellfire_ammo),
|
||||
"liberator_bomb" -> MakeAmmoBox(liberator_bomb),
|
||||
|
|
@ -129,7 +127,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
* key - an identification string sent by the client
|
||||
* value - a curried function that builds the object
|
||||
*/
|
||||
protected val infantryWeapons : HashMap[String, ()=>Equipment] = HashMap(
|
||||
protected val infantryWeapons : Map[String, ()=>Equipment] = Map(
|
||||
"ilc9" -> MakeTool(ilc9, bullet_9mm),
|
||||
"repeater" -> MakeTool(repeater, bullet_9mm),
|
||||
"isp" -> MakeTool(isp, shotgun_shell), //amp
|
||||
|
|
@ -173,7 +171,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
|
|||
* key - an identification string sent by the client
|
||||
* value - a curried function that builds the object
|
||||
*/
|
||||
protected val supportWeapons : HashMap[String, ()=>Equipment] = HashMap(
|
||||
protected val supportWeapons : Map[String, ()=>Equipment] = Map(
|
||||
"medkit" -> MakeKit(medkit),
|
||||
"super_medkit" -> MakeKit(super_medkit),
|
||||
"super_armorkit" -> MakeKit(super_armorkit),
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.terminals
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
//temporary location for these temporary messages
|
||||
object TemporaryTerminalMessages {
|
||||
//TODO send original packets along with these messages
|
||||
final case class Convert(faction : PlanetSideEmpire.Value)
|
||||
final case class Hacked(faction : Option[PlanetSideEmpire.Value])
|
||||
final case class Damaged(dm : Int)
|
||||
final case class Repaired(rep : Int)
|
||||
}
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
package net.psforever.objects.zones
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.serverobject.doors.Base
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor}
|
||||
import net.psforever.objects.guid.actor.UniqueNumberSystem
|
||||
import net.psforever.objects.guid.selector.RandomSelector
|
||||
import net.psforever.objects.guid.source.LimitedNumberSource
|
||||
import net.psforever.packet.GamePacket
|
||||
|
|
@ -41,11 +42,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
|||
private var accessor : ActorRef = ActorRef.noSender
|
||||
/** The basic support structure for the globally unique number system used by this `Zone`. */
|
||||
private var guid : NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(65536))
|
||||
guid.AddPool("environment", (0 to 2000).toList)
|
||||
guid.AddPool("dynamic", (2001 to 10000).toList).Selector = new RandomSelector //TODO unlump pools later; do not make too big
|
||||
/** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
|
||||
private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]()
|
||||
/** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */
|
||||
private var ground : ActorRef = ActorRef.noSender
|
||||
|
||||
private var bases : List[Base] = List()
|
||||
|
||||
/**
|
||||
* Establish the basic accessible conditions necessary for a functional `Zone`.<br>
|
||||
* <br>
|
||||
|
|
@ -60,18 +65,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
|||
*/
|
||||
def Init(implicit context : ActorContext) : Unit = {
|
||||
if(accessor == ActorRef.noSender) {
|
||||
//TODO wrong initialization for GUID
|
||||
implicit val guid = this.guid
|
||||
//passed into builderObject.Build implicitly
|
||||
val pool = guid.AddPool("pool", (200 to 1000).toList)
|
||||
pool.Selector = new RandomSelector
|
||||
val poolActor = context.actorOf(Props(classOf[NumberPoolActor], pool), name = s"$Id-poolActor")
|
||||
accessor = context.actorOf(Props(classOf[NumberPoolAccessorActor], guid, pool, poolActor), s"$Id-accessor")
|
||||
implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly
|
||||
accessor = context.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid)), s"$Id-uns")
|
||||
ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground")
|
||||
|
||||
Map.LocalObjects.foreach({ builderObject =>
|
||||
builderObject.Build
|
||||
})
|
||||
|
||||
MakeBases(Map.LocalBases)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +174,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
|
|||
*/
|
||||
def Ground : ActorRef = ground
|
||||
|
||||
def MakeBases(num : Int) : List[Base] = {
|
||||
bases = (0 to num).map(id => new Base(id)).toList
|
||||
bases
|
||||
}
|
||||
|
||||
def Base(id : Int) : Option[Base] = {
|
||||
bases.lift(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide bulk correspondence on all map entities that can be composed into packet messages and reported to a client.
|
||||
* These messages are sent in this fashion at the time of joining the server:<br>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.zones
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.serverobject.locks.IFFLock
|
||||
|
||||
/**
|
||||
* na
|
||||
|
|
@ -13,8 +14,48 @@ class ZoneActor(zone : Zone) extends Actor {
|
|||
def receive : Receive = {
|
||||
case Zone.Init() =>
|
||||
zone.Init
|
||||
ZoneSetupCheck()
|
||||
|
||||
case msg =>
|
||||
log.warn(s"Received unexpected message - $msg")
|
||||
}
|
||||
|
||||
def ZoneSetupCheck(): Unit = {
|
||||
def guid(id : Int) = zone.GUID(id)
|
||||
val map = zone.Map
|
||||
val slog = org.log4s.getLogger(s"zone/${zone.Id}/sanity")
|
||||
|
||||
//check base to object associations
|
||||
map.ObjectToBase.foreach({ case((object_guid, base_id)) =>
|
||||
if(zone.Base(base_id).isEmpty) {
|
||||
slog.error(s"expected a base #$base_id")
|
||||
}
|
||||
if(guid(object_guid).isEmpty) {
|
||||
slog.error(s"expected object id $object_guid to exist, but it did not")
|
||||
}
|
||||
})
|
||||
|
||||
//check door to locks association
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
map.DoorToLock.foreach({ case((door_guid, lock_guid)) =>
|
||||
try {
|
||||
if(!guid(door_guid).get.isInstanceOf[Door]) {
|
||||
slog.error(s"expected id $door_guid to be a door, but it was not")
|
||||
}
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
slog.error(s"expected a door, but looking for uninitialized object $door_guid")
|
||||
}
|
||||
try {
|
||||
if(!guid(lock_guid).get.isInstanceOf[IFFLock]) {
|
||||
slog.error(s"expected id $lock_guid to be an IFF locks, but it was not")
|
||||
}
|
||||
}
|
||||
catch {
|
||||
case _ : Exception =>
|
||||
slog.error(s"expected an IFF locks, but looking for uninitialized object $lock_guid")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.zones
|
||||
|
||||
import net.psforever.objects.serverobject.builders.ServerObjectBuilder
|
||||
|
||||
/**
|
||||
* The fixed instantiation and relation of a series of server objects.<br>
|
||||
* <br>
|
||||
|
|
@ -12,32 +14,55 @@ package net.psforever.objects.zones
|
|||
* Use it as a blueprint.<br>
|
||||
* <br>
|
||||
* The "training zones" are the best example of the difference between a `ZoneMap` and a `Zone.`
|
||||
* ("Course" will be used as an unofficial location and layout descriptor.)
|
||||
* `tzdrtr` is the Terran Republic driving course.
|
||||
* `tzdrvs` is the Vanu Sovereignty driving course.
|
||||
* While each course can have different objects and object states (`Zone`),
|
||||
* both courses have the same basic server objects because they are built from the same blueprint (`ZoneMap`).
|
||||
* While each course can have different objects and object states, i.e., a `Zone`,
|
||||
* both of these courses utilize the same basic server object layout because they are built from the same blueprint, i.e., a `ZoneMap`.
|
||||
* @param name the privileged name that can be used as the first parameter in the packet `LoadMapMessage`
|
||||
* @see `ServerObjectBuilder`<br>
|
||||
* `LoadMapMessage`
|
||||
*/
|
||||
class ZoneMap(private val name : String) {
|
||||
private var localObjects : List[ServerObjectBuilder] = List()
|
||||
private var localObjects : List[ServerObjectBuilder[_]] = List()
|
||||
private var linkDoorLock : Map[Int, Int] = Map()
|
||||
private var linkObjectBase : Map[Int, Int] = Map()
|
||||
private var numBases : Int = 0
|
||||
|
||||
def Name : String = name
|
||||
|
||||
/**
|
||||
* Append the builder for a server object to the list of builders known to this `ZoneMap`.
|
||||
* @param obj the builder for a server object
|
||||
*/
|
||||
def LocalObject(obj : ServerObjectBuilder) : Unit = {
|
||||
localObjects = localObjects :+ obj
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of all server object builder wrappers that have been assigned to this `ZoneMap`.
|
||||
* @return the `List` of all `ServerObjectBuilders` known to this `ZoneMap`
|
||||
*/
|
||||
def LocalObjects : List[ServerObjectBuilder] = {
|
||||
def LocalObjects : List[ServerObjectBuilder[_]] = {
|
||||
localObjects
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the builder for a server object to the list of builders known to this `ZoneMap`.
|
||||
* @param obj the builder for a server object
|
||||
*/
|
||||
def LocalObject(obj : ServerObjectBuilder[_]) : Unit = {
|
||||
localObjects = localObjects :+ obj
|
||||
}
|
||||
|
||||
def LocalBases : Int = numBases
|
||||
|
||||
def LocalBases_=(num : Int) : Int = {
|
||||
numBases = math.max(0, num)
|
||||
LocalBases
|
||||
}
|
||||
|
||||
def ObjectToBase : Map[Int, Int] = linkObjectBase
|
||||
|
||||
def ObjectToBase(object_guid : Int, base_id : Int) : Unit = {
|
||||
linkObjectBase = linkObjectBase ++ Map(object_guid -> base_id)
|
||||
}
|
||||
|
||||
def DoorToLock : Map[Int, Int] = linkDoorLock
|
||||
|
||||
def DoorToLock(door_guid : Int, lock_guid : Int) = {
|
||||
linkDoorLock = linkDoorLock ++ Map(door_guid -> lock_guid)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,8 +358,8 @@ object GamePacketOpcode extends Enumeration {
|
|||
|
||||
// OPCODES 0x20-2f
|
||||
case 0x20 => noDecoder(UnknownMessage32)
|
||||
case 0x21 => noDecoder(ActionProgressMessage)
|
||||
case 0x22 => noDecoder(ActionCancelMessage)
|
||||
case 0x21 => game.ActionProgressMessage.decode
|
||||
case 0x22 => game.ActionCancelMessage.decode
|
||||
case 0x23 => noDecoder(ActionCancelAcknowledgeMessage)
|
||||
case 0x24 => game.SetEmpireMessage.decode
|
||||
case 0x25 => game.EmoteMsg.decode
|
||||
|
|
@ -418,7 +418,7 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0x51 => game.TriggerEffectMessage.decode
|
||||
case 0x52 => game.WeaponDryFireMessage.decode
|
||||
case 0x53 => noDecoder(DroppodLaunchRequestMessage)
|
||||
case 0x54 => noDecoder(HackMessage)
|
||||
case 0x54 => game.HackMessage.decode
|
||||
case 0x55 => noDecoder(DroppodLaunchResponseMessage)
|
||||
case 0x56 => noDecoder(GenericObjectActionMessage)
|
||||
case 0x57 => game.AvatarVehicleTimerMessage.decode
|
||||
|
|
@ -445,7 +445,7 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0x68 => noDecoder(DroppodFreefallingMessage)
|
||||
case 0x69 => game.AvatarFirstTimeEventMessage.decode
|
||||
case 0x6a => noDecoder(AggravatedDamageMessage)
|
||||
case 0x6b => noDecoder(TriggerSoundMessage)
|
||||
case 0x6b => game.TriggerSoundMessage.decode
|
||||
case 0x6c => noDecoder(LootItemMessage)
|
||||
case 0x6d => noDecoder(VehicleSubStateMessage)
|
||||
case 0x6e => noDecoder(SquadMembershipRequest)
|
||||
|
|
@ -515,7 +515,7 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0xa3 => noDecoder(UplinkResponse)
|
||||
case 0xa4 => game.WarpgateRequest.decode
|
||||
case 0xa5 => noDecoder(WarpgateResponse)
|
||||
case 0xa6 => noDecoder(DamageWithPositionMessage)
|
||||
case 0xa6 => game.DamageWithPositionMessage.decode
|
||||
case 0xa7 => game.GenericActionMessage.decode
|
||||
// 0xa8
|
||||
case 0xa8 => game.ContinentalLockUpdateMessage.decode
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param player_guid na
|
||||
* @param object_guid na
|
||||
* @param unk na
|
||||
*/
|
||||
final case class ActionCancelMessage(player_guid : PlanetSideGUID,
|
||||
object_guid : PlanetSideGUID,
|
||||
unk : Int)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = ActionCancelMessage
|
||||
def opcode = GamePacketOpcode.ActionCancelMessage
|
||||
def encode = ActionCancelMessage.encode(this)
|
||||
}
|
||||
|
||||
object ActionCancelMessage extends Marshallable[ActionCancelMessage] {
|
||||
implicit val codec : Codec[ActionCancelMessage] = (
|
||||
("player_guid" | PlanetSideGUID.codec) ::
|
||||
("object_guid" | PlanetSideGUID.codec) ::
|
||||
("unk" | uint4L)
|
||||
).as[ActionCancelMessage]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
final case class ActionProgressMessage(unk1 : Int,
|
||||
unk2 : Long)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = ActionProgressMessage
|
||||
def opcode = GamePacketOpcode.ActionProgressMessage
|
||||
def encode = ActionProgressMessage.encode(this)
|
||||
}
|
||||
|
||||
object ActionProgressMessage extends Marshallable[ActionProgressMessage] {
|
||||
implicit val codec : Codec[ActionProgressMessage] = (
|
||||
("unk1" | uint4L) ::
|
||||
("unk2" | uint32L)
|
||||
).as[ActionProgressMessage]
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import net.psforever.types.Vector3
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
* Dispatched by the server to indicate a source of damage affecting the player.
|
||||
* Unlike `HitHint` the damage source is defined by an actual coordinate location rather than a physical target.<br>
|
||||
* <br>
|
||||
* The player will be shown a fading, outwards drifting, red tick mark.
|
||||
* The location will indicate a general direction towards the source.
|
||||
* If the option `Game/Show Damage Flash` is set, the player's screen will flash red briefly when a mark is displayed.
|
||||
* @param unk the intensity of the damage tick marks
|
||||
* @param pos the position
|
||||
* @see `HitHint`
|
||||
*/
|
||||
final case class DamageWithPositionMessage(unk : Int,
|
||||
pos : Vector3)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = DamageWithPositionMessage
|
||||
def opcode = GamePacketOpcode.DamageWithPositionMessage
|
||||
def encode = DamageWithPositionMessage.encode(this)
|
||||
}
|
||||
|
||||
object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage] {
|
||||
implicit val codec : Codec[DamageWithPositionMessage] = (
|
||||
("unk" | uint8L) ::
|
||||
("pos" | Vector3.codec_pos)
|
||||
).as[DamageWithPositionMessage]
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
* An `Enumeration` of the various states and activities of the hacking process.
|
||||
* These values are closely tied to the condition of the hacking progress bar and/or the condition of the hacked object.<br>
|
||||
* <br>
|
||||
* `Start` initially displays the hacking progress bar.<br>
|
||||
* `Ongoing` is a neutral state that keeps the progress bar displayed while its value updates. (unconfirmed?)<br>
|
||||
* `Finished` disposes of the hacking progress bar. It does not, by itself, mean the hack was successful.<br>
|
||||
* `Hacked` modifies the target of the hack.<br>
|
||||
* `HackCleared` modifies the target of the hack, opposite of `Hacked`.
|
||||
*/
|
||||
object HackState extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
val
|
||||
Unknown0,
|
||||
Start,
|
||||
Unknown2,
|
||||
Ongoing,
|
||||
Finished,
|
||||
Unknown5,
|
||||
Hacked,
|
||||
HackCleared
|
||||
= Value
|
||||
|
||||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatched by the server to control the process of hacking.<br>
|
||||
* <br>
|
||||
* Part of the hacking process is regulated by the server while another part of it is automatically reset by the client.
|
||||
* The visibility, update, and closing of the hacking progress bar must be handled manually, for each tick.
|
||||
* When hacking is complete, using the appropriate `HackState` will cue the target to be affected by the hack.
|
||||
* Terminals and door IFF panels will temporarily expose their functionality;
|
||||
* the faction association of vehicles will be converted permanently;
|
||||
* a protracted process of a base conversion will be enacted; etc..
|
||||
* This transfer of faction association occurs to align the target with the faction of the hacking player (as indicated).
|
||||
* The client will select the faction without needing to be explicitly told
|
||||
* and will select the appropriate action to enact upon the target.
|
||||
* Upon the hack's completion, the target on the client will automatically revert back to its original state, if possible.
|
||||
* (It will still be necessary to alert this change from the server's perspective.)
|
||||
* @param unk1 na;
|
||||
* hack type?
|
||||
* @param target_guid the target of the hack
|
||||
* @param player_guid the player
|
||||
* @param progress the amount of progress visible;
|
||||
* visible range is 0 - 100
|
||||
* @param unk5 na;
|
||||
* often a large number;
|
||||
* doesn't seem to be `char_id`?
|
||||
* @param hack_state hack state
|
||||
* @param unk7 na;
|
||||
* usually, 8?
|
||||
*/
|
||||
final case class HackMessage(unk1 : Int,
|
||||
target_guid : PlanetSideGUID,
|
||||
player_guid : PlanetSideGUID,
|
||||
progress : Int,
|
||||
unk5 : Long,
|
||||
hack_state : HackState.Value,
|
||||
unk7 : Long)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = HackMessage
|
||||
def opcode = GamePacketOpcode.HackMessage
|
||||
def encode = HackMessage.encode(this)
|
||||
}
|
||||
|
||||
object HackMessage extends Marshallable[HackMessage] {
|
||||
implicit val codec : Codec[HackMessage] = (
|
||||
("unk1" | uint2L) ::
|
||||
("object_guid" | PlanetSideGUID.codec) ::
|
||||
("player_guid" | PlanetSideGUID.codec) ::
|
||||
("progress" | uint8L) ::
|
||||
("unk5" | uint32L) ::
|
||||
("hack_state" | HackState.codec) ::
|
||||
("unk7" | uint32L)
|
||||
).as[HackMessage]
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import scodec.codecs._
|
|||
* `17 - BEP. Value seems to be the same as BattleExperienceMessage`<br>
|
||||
* `18 - CEP.`<br>
|
||||
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`<br>
|
||||
* `24 - Certifications with value :`<br>
|
||||
* `24 - Learn certifications with value :`<br>
|
||||
* 01 : Medium Assault<br>
|
||||
* 02 : Heavy Assault<br>
|
||||
* 03 : Special Assault<br>
|
||||
|
|
@ -66,6 +66,7 @@ import scodec.codecs._
|
|||
* 43 : Fortification Engineering<br>
|
||||
* 44 : Assault Engineering<br>
|
||||
* 45 : Advanced Engineering (= Fortification Engineering + Assault Engineering) Must have Combat Engineering<br>
|
||||
* `25 - Forget certifications (same order as 24)`
|
||||
* `29 - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`<br>
|
||||
* `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`<br>
|
||||
* `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`<br>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import net.psforever.types.Vector3
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
/**
|
||||
* An `Enumeration` of the sounds triggered by this packet.
|
||||
* Twenty-one possible sounds are available for playback.
|
||||
*/
|
||||
object TriggeredSound extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
val
|
||||
SpawnInTube,
|
||||
Unknown1,
|
||||
Hack,
|
||||
HackDoor,
|
||||
Unknown4,
|
||||
LockedOut,
|
||||
Unknown6,
|
||||
Unknown7,
|
||||
Unknown8,
|
||||
Unknown9,
|
||||
Unknown10,
|
||||
Unknown11,
|
||||
Unknown12,
|
||||
Unknown13,
|
||||
Unknown14,
|
||||
Unknown15,
|
||||
Unknown16,
|
||||
Unknown17,
|
||||
Unknown18,
|
||||
Unknown19,
|
||||
Unknown20 = Value
|
||||
|
||||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(5))
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatched by the server to cause a sound to be played at a certain location in the world.
|
||||
* @param sound the kind of sound
|
||||
* @param pos the location where the sound gets played
|
||||
* @param unk na;
|
||||
* may be radius
|
||||
* @param volume the volume of the sound at the origin (0.0f - 1.0f)
|
||||
*/
|
||||
final case class TriggerSoundMessage(sound : TriggeredSound.Value,
|
||||
pos : Vector3,
|
||||
unk : Int,
|
||||
volume : Float)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = TriggerSoundMessage
|
||||
def opcode = GamePacketOpcode.TriggerSoundMessage
|
||||
def encode = TriggerSoundMessage.encode(this)
|
||||
}
|
||||
|
||||
object TriggerSoundMessage extends Marshallable[TriggerSoundMessage] {
|
||||
implicit val codec : Codec[TriggerSoundMessage] = (
|
||||
("sound" | TriggeredSound.codec) ::
|
||||
("pos" | Vector3.codec_pos) ::
|
||||
("unk" | uintL(9)) ::
|
||||
("volume" | uint8L)
|
||||
).xmap[TriggerSoundMessage] (
|
||||
{
|
||||
case a :: b :: c :: d :: HNil =>
|
||||
TriggerSoundMessage(a, b, c, d.toFloat * 0.0039215689f)
|
||||
},
|
||||
{
|
||||
case TriggerSoundMessage(a, b, c, d) =>
|
||||
a :: b :: c :: (d * 255f).toInt :: HNil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -41,24 +41,6 @@ object UniformStyle extends Enumeration {
|
|||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3))
|
||||
}
|
||||
|
||||
/**
|
||||
* The different cosmetics that a player can apply to their model's head.<br>
|
||||
* <br>
|
||||
* The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
|
||||
* @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
|
||||
* all other cosmetics require `no_helmet` to be `true` before they can be seen
|
||||
* @param beret player dons a beret
|
||||
* @param sunglasses player dons sunglasses
|
||||
* @param earpiece player dons an earpiece on the left
|
||||
* @param brimmed_cap player dons a cap;
|
||||
* the cap overrides the beret, if both are selected
|
||||
*/
|
||||
final case class Cosmetics(no_helmet : Boolean,
|
||||
beret : Boolean,
|
||||
sunglasses : Boolean,
|
||||
earpiece : Boolean,
|
||||
brimmed_cap : Boolean)
|
||||
|
||||
/**
|
||||
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
|
||||
* This densely-packed information outlines most of the specifics of depicting some other character.<br>
|
||||
|
|
@ -118,7 +100,7 @@ final case class CharacterData(appearance : CharacterAppearanceData,
|
|||
//factor guard bool values into the base size, not its corresponding optional field
|
||||
val appearanceSize : Long = appearance.bitsize
|
||||
val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
|
||||
val cosmeticsSize : Long = if(cosmetics.isDefined) { 5L } else { 0L }
|
||||
val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L }
|
||||
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
|
||||
32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize
|
||||
}
|
||||
|
|
@ -141,19 +123,6 @@ object CharacterData extends Marshallable[CharacterData] {
|
|||
def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData =
|
||||
new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot)
|
||||
|
||||
/**
|
||||
* Check for the bit flags for the cosmetic items.
|
||||
* These flags are only valid if the player has acquired their third uniform upgrade.
|
||||
* @see `UniformStyle.ThirdUpgrade`
|
||||
*/
|
||||
private val cosmeticsCodec : Codec[Cosmetics] = (
|
||||
("no_helmet" | bool) ::
|
||||
("beret" | bool) ::
|
||||
("sunglasses" | bool) ::
|
||||
("earpiece" | bool) ::
|
||||
("brimmed_cap" | bool)
|
||||
).as[Cosmetics]
|
||||
|
||||
implicit val codec : Codec[CharacterData] = (
|
||||
("app" | CharacterAppearanceData.codec) ::
|
||||
("health" | uint8L) :: //dead state when health == 0
|
||||
|
|
@ -163,7 +132,7 @@ object CharacterData extends Marshallable[CharacterData] {
|
|||
("command_rank" | uintL(3)) ::
|
||||
bool :: //stream misalignment when != 1
|
||||
optional(bool, "implant_effects" | ImplantEffects.codec) ::
|
||||
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | cosmeticsCodec) ::
|
||||
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) ::
|
||||
optional(bool, "inventory" | InventoryData.codec) ::
|
||||
("drawn_slot" | DrawnSlot.codec) ::
|
||||
bool //usually false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game.objectcreate
|
||||
|
||||
import scodec.codecs._
|
||||
import scodec.Codec
|
||||
|
||||
/**
|
||||
* The different cosmetics that a player can apply to their character model's head.<br>
|
||||
* <br>
|
||||
* The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
|
||||
* These flags are only valid if the player has:
|
||||
* for `DetailedCharacterData`, achieved at least battle rank twenty-four (battle experience points greater than 2286230),
|
||||
* or, for `CharacterData`, achieved at least battle rank twenty-five (acquired their third uniform upgrade).
|
||||
* `CharacterData`, as implied, will not display these options until one battle rank after they would have become available.
|
||||
* @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
|
||||
* all other cosmetics require `no_helmet` to be `true` before they can be seen
|
||||
* @param beret player dons a beret
|
||||
* @param sunglasses player dons sunglasses
|
||||
* @param earpiece player dons an earpiece on the left
|
||||
* @param brimmed_cap player dons a cap;
|
||||
* the cap overrides the beret, if both are selected
|
||||
* @see `UniformStyle.ThirdUpgrade`
|
||||
*/
|
||||
final case class Cosmetics(no_helmet : Boolean,
|
||||
beret : Boolean,
|
||||
sunglasses : Boolean,
|
||||
earpiece : Boolean,
|
||||
brimmed_cap : Boolean
|
||||
) extends StreamBitSize {
|
||||
override def bitsize : Long = 5L
|
||||
}
|
||||
|
||||
object Cosmetics {
|
||||
implicit val codec : Codec[Cosmetics] = (
|
||||
("no_helmet" | bool) ::
|
||||
("beret" | bool) ::
|
||||
("sunglasses" | bool) ::
|
||||
("earpiece" | bool) ::
|
||||
("brimmed_cap" | bool)
|
||||
).as[Cosmetics]
|
||||
}
|
||||
|
|
@ -70,6 +70,9 @@ final case class ImplantEntry(implant : ImplantType.Value,
|
|||
* @param tutorials the `List` of tutorials completed by this avatar;
|
||||
* the size field is a 32-bit number;
|
||||
* the first entry may be padded
|
||||
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
|
||||
* they become available at battle rank 24;
|
||||
* these flags do not exist if they are not applicable
|
||||
* @param inventory the avatar's inventory
|
||||
* @param drawn_slot the holster that is initially drawn
|
||||
* @see `CharacterAppearanceData`<br>
|
||||
|
|
@ -93,6 +96,7 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
|
|||
implants : List[ImplantEntry],
|
||||
firstTimeEvents : List[String],
|
||||
tutorials : List[String],
|
||||
cosmetics : Option[Cosmetics],
|
||||
inventory : Option[InventoryData],
|
||||
drawn_slot : DrawnSlot.Value = DrawnSlot.None
|
||||
) extends ConstructorData {
|
||||
|
|
@ -116,13 +120,16 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
|
|||
for(str <- tutorials) {
|
||||
tutorialListSize += StreamBitSize.stringBitSize(str)
|
||||
}
|
||||
val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24
|
||||
val extraBitSize : Long = if(br24) { 33L } else { 46L }
|
||||
val cosmeticsSize : Long = if(br24) { cosmetics.get.bitsize } else { 0L }
|
||||
val inventorySize : Long = if(inventory.isDefined) { //inventory
|
||||
inventory.get.bitsize
|
||||
}
|
||||
else {
|
||||
0L
|
||||
}
|
||||
649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
|
||||
603L + appearanceSize + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize + inventorySize
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,8 +152,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
* @param drawn_slot the holster that is initially drawn
|
||||
* @return a `DetailedCharacterData` object
|
||||
*/
|
||||
def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
|
||||
new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
|
||||
def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
|
||||
new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics, Some(inventory), drawn_slot)
|
||||
|
||||
/**
|
||||
* `Codec` for entries in the `List` of implants.
|
||||
|
|
@ -179,7 +186,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
* @param bep battle experience points
|
||||
* @return the number of accessible implant slots
|
||||
*/
|
||||
private def numberOfImplantSlots(bep : Long) : Int = {
|
||||
def numberOfImplantSlots(bep : Long) : Int = {
|
||||
if(bep > 754370) { //BR18+
|
||||
3
|
||||
}
|
||||
|
|
@ -209,7 +216,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
implantOffset += entry.bitsize.toInt
|
||||
})
|
||||
val resultB : Int = resultA - (implantOffset % 8)
|
||||
if(resultB < 0) { 8 - resultB } else { resultB }
|
||||
if(resultB < 0) { 8 + resultB } else { resultB }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -269,6 +276,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
}
|
||||
}
|
||||
|
||||
def isBR24(bep : Long) : Boolean = bep > 2286230
|
||||
|
||||
implicit val codec : Codec[DetailedCharacterData] = (
|
||||
("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
|
||||
("bep" | uint32L) >>:~ { bep =>
|
||||
|
|
@ -297,10 +306,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
(("tutorial_length" | uint32L) >>:~ { len2 =>
|
||||
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
|
||||
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
|
||||
ignore(207) ::
|
||||
optional(bool, "inventory" | InventoryData.codec_detailed) ::
|
||||
("drawn_slot" | DrawnSlot.codec) ::
|
||||
bool //usually false
|
||||
ignore(160) ::
|
||||
(bool >>:~ { br24 => //BR24+
|
||||
newcodecs.binary_choice(br24, ignore(33), ignore(46)) ::
|
||||
conditional(br24, Cosmetics.codec) ::
|
||||
optional(bool, "inventory" | InventoryData.codec_detailed) ::
|
||||
("drawn_slot" | DrawnSlot.codec) ::
|
||||
bool //usually false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -308,14 +321,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
}
|
||||
).exmap[DetailedCharacterData] (
|
||||
{
|
||||
case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil =>
|
||||
case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: inv :: drawn :: false :: HNil =>
|
||||
//prepend the displaced first elements to their lists
|
||||
val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1
|
||||
val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1
|
||||
Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn))
|
||||
val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else { fte1 }
|
||||
val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else { tut1 }
|
||||
Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics, inv, drawn))
|
||||
},
|
||||
{
|
||||
case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn) =>
|
||||
case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos, inv, drawn) =>
|
||||
val implantCapacity : Int = numberOfImplantSlots(bep)
|
||||
val implantList = if(implants.length > implantCapacity) {
|
||||
implants.slice(0, implantCapacity)
|
||||
|
|
@ -334,7 +347,9 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
|||
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
|
||||
case Nil => (None, Nil)
|
||||
}
|
||||
Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil)
|
||||
val br24 : Boolean = isBR24(bep)
|
||||
val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None }
|
||||
Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: inv :: drawn :: false :: HNil)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ import shapeless.{::, HNil}
|
|||
* This data will help construct the "tool" called a Remote Electronics Kit.<br>
|
||||
* <br>
|
||||
* Of note is the first portion of the data which resembles the `DetailedWeaponData` format.
|
||||
* @param unk na
|
||||
* @param unk1 na
|
||||
* @param unk2 na
|
||||
*/
|
||||
final case class DetailedREKData(unk : Int) extends ConstructorData {
|
||||
final case class DetailedREKData(unk1 : Int,
|
||||
unk2 : Int = 0
|
||||
) extends ConstructorData {
|
||||
override def bitsize : Long = 67L
|
||||
}
|
||||
|
||||
|
|
@ -25,17 +28,17 @@ object DetailedREKData extends Marshallable[DetailedREKData] {
|
|||
uint4L ::
|
||||
uint16L ::
|
||||
uint4L ::
|
||||
uintL(15)
|
||||
("unk2" | uintL(15))
|
||||
).exmap[DetailedREKData] (
|
||||
{
|
||||
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
|
||||
Attempt.successful(DetailedREKData(code))
|
||||
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil =>
|
||||
Attempt.successful(DetailedREKData(code, unk2))
|
||||
case _ =>
|
||||
Attempt.failure(Err("invalid rek data format"))
|
||||
},
|
||||
{
|
||||
case DetailedREKData(code) =>
|
||||
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
|
||||
case DetailedREKData(code, unk2) =>
|
||||
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ object CertificationType extends Enumeration {
|
|||
AntiVehicular,
|
||||
Sniping,
|
||||
EliteAssault,
|
||||
AirCalvaryScout,
|
||||
AirCalvaryInterceptor,
|
||||
AirCalvaryAssault,
|
||||
AirCavalryScout,
|
||||
AirCavalryInterceptor,
|
||||
AirCavalryAssault,
|
||||
//10
|
||||
AirSupport,
|
||||
ATV,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import scodec.codecs._
|
|||
*/
|
||||
object ImplantType extends Enumeration {
|
||||
type Type = Value
|
||||
val AdvancedRegen,
|
||||
|
||||
val
|
||||
AdvancedRegen,
|
||||
Targeting,
|
||||
AudioAmplifier,
|
||||
DarklightVision,
|
||||
|
|
|
|||
29
common/src/test/scala/game/ActionCancelMessageTest.scala
Normal file
29
common/src/test/scala/game/ActionCancelMessageTest.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import scodec.bits._
|
||||
|
||||
class ActionCancelMessageTest extends Specification {
|
||||
val string = hex"22 201ee01a10"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case ActionCancelMessage(player_guid, object_guid, unk) =>
|
||||
player_guid mustEqual PlanetSideGUID(7712)
|
||||
object_guid mustEqual PlanetSideGUID(6880)
|
||||
unk mustEqual 1
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = ActionCancelMessage(PlanetSideGUID(7712), PlanetSideGUID(6880), 1)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
29
common/src/test/scala/game/ActionProgressMessageTest.scala
Normal file
29
common/src/test/scala/game/ActionProgressMessageTest.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.ExoSuitType
|
||||
import scodec.bits._
|
||||
|
||||
class ActionProgressMessageTest extends Specification {
|
||||
val string = hex"216000000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case ActionProgressMessage(unk1, unk2) =>
|
||||
unk1 mustEqual 6
|
||||
unk2 mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = ActionProgressMessage(6, 0L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package game
|
||||
|
||||
import net.psforever.types.{MeritCommendation, Vector3}
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import scodec.bits._
|
||||
|
||||
class DamageWithPositionMessageTest extends Specification {
|
||||
val string = hex"A6 11 6C2D7 65535 CA16"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case DamageWithPositionMessage(unk, pos) =>
|
||||
unk mustEqual 17
|
||||
pos.x mustEqual 3674.8438f
|
||||
pos.y mustEqual 2726.789f
|
||||
pos.z mustEqual 91.15625f
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = DamageWithPositionMessage(17, Vector3(3674.8438f, 2726.789f, 91.15625f))
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
33
common/src/test/scala/game/HackMessageTest.scala
Normal file
33
common/src/test/scala/game/HackMessageTest.scala
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import scodec.bits._
|
||||
|
||||
class HackMessageTest extends Specification {
|
||||
// Record 62 in PSCap-hack-door-tower.gcap
|
||||
val string = hex"54 000105c3800000202fc04200000000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case HackMessage(unk1, target_guid, player_guid, progress, unk5, hack_state, unk7) =>
|
||||
unk1 mustEqual 0
|
||||
target_guid mustEqual PlanetSideGUID(1024)
|
||||
player_guid mustEqual PlanetSideGUID(3607)
|
||||
progress mustEqual 0
|
||||
unk5 mustEqual 3212836864L
|
||||
hack_state mustEqual HackState.Start
|
||||
unk7 mustEqual 8L
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = HackMessage(0, PlanetSideGUID(1024), PlanetSideGUID(3607), 0, 3212836864L, HackState.Start, 8L)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
33
common/src/test/scala/game/TriggerSoundMessageTest.scala
Normal file
33
common/src/test/scala/game/TriggerSoundMessageTest.scala
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.Vector3
|
||||
import scodec.bits._
|
||||
|
||||
class TriggerSoundMessageTest extends Specification {
|
||||
val string = hex"6B 1FD5E1B466DB3858F1FC"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.DecodePacket(string).require match {
|
||||
case TriggerSoundMessage(sound, pos, unk2, volume) =>
|
||||
sound mustEqual TriggeredSound.HackDoor
|
||||
pos.x mustEqual 1913.9531f
|
||||
pos.y mustEqual 6042.8125f
|
||||
pos.z mustEqual 45.609375f
|
||||
unk2 mustEqual 30
|
||||
volume mustEqual 0.49803925f
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = TriggerSoundMessage(TriggeredSound.HackDoor, Vector3(1913.9531f, 6042.8125f, 45.609375f), 30, 0.49803925f)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
}
|
||||
}
|
||||
13
common/src/test/scala/objects/ActorTest.scala
Normal file
13
common/src/test/scala/objects/ActorTest.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package objects
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.testkit.{ImplicitSender, TestKit}
|
||||
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
|
||||
import org.specs2.specification.Scope
|
||||
|
||||
abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
|
||||
override def afterAll {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package objects
|
||||
|
||||
import net.psforever.objects.definition.converter.{ACEConverter, REKConverter}
|
||||
import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, REKConverter}
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.definition._
|
||||
import net.psforever.objects.equipment.CItem.{DeployedItem, Unit}
|
||||
|
|
@ -130,7 +130,7 @@ class ConverterTest extends Specification {
|
|||
}
|
||||
|
||||
"Player" should {
|
||||
"convert to packet" in {
|
||||
val obj : Player = {
|
||||
/*
|
||||
Create an AmmoBoxDefinition with which to build two AmmoBoxes
|
||||
Create a ToolDefinition with which to create a Tool
|
||||
|
|
@ -157,9 +157,64 @@ class ConverterTest extends Specification {
|
|||
obj.Slot(2).Equipment = tool
|
||||
obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(94)
|
||||
obj.Inventory += 8 -> box2
|
||||
obj
|
||||
}
|
||||
val converter = new CharacterSelectConverter
|
||||
|
||||
obj.Definition.Packet.DetailedConstructorData(obj).isSuccess mustEqual true
|
||||
ok //TODO write more of this test
|
||||
"convert to packet (BR < 24)" in {
|
||||
obj.BEP = 0
|
||||
obj.Definition.Packet.DetailedConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
obj.Definition.Packet.ConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"convert to packet (BR >= 24)" in {
|
||||
obj.BEP = 10000000
|
||||
obj.Definition.Packet.DetailedConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
obj.Definition.Packet.ConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"convert to simple packet (BR < 24)" in {
|
||||
obj.BEP = 0
|
||||
converter.DetailedConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
converter.ConstructorData(obj).isFailure mustEqual true
|
||||
converter.ConstructorData(obj).get must throwA[Exception]
|
||||
}
|
||||
|
||||
"convert to simple packet (BR >= 24)" in {
|
||||
obj.BEP = 10000000
|
||||
converter.DetailedConstructorData(obj) match {
|
||||
case Success(pkt) =>
|
||||
ok
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
converter.ConstructorData(obj).isFailure mustEqual true
|
||||
converter.ConstructorData(obj).get must throwA[Exception]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,11 @@
|
|||
package objects
|
||||
|
||||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
|
||||
import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor, Register}
|
||||
import net.psforever.objects.guid.actor.NumberPoolActor
|
||||
import net.psforever.objects.guid.pool.ExclusivePool
|
||||
import net.psforever.objects.guid.selector.RandomSelector
|
||||
import net.psforever.objects.guid.source.LimitedNumberSource
|
||||
import org.specs2.specification.Scope
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import scala.util.Success
|
||||
|
||||
abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
|
||||
override def afterAll {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
}
|
||||
|
||||
class NumberPoolActorTest extends ActorTest(ActorSystem("test")) {
|
||||
"NumberPoolActor" should {
|
||||
|
|
@ -61,23 +48,3 @@ class NumberPoolActorTest2 extends ActorTest(ActorSystem("test")) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NumberPoolActorTest3 extends ActorTest(ActorSystem("test")) {
|
||||
"NumberPoolAccessorActor" should {
|
||||
class TestEntity extends IdentifiableEntity
|
||||
|
||||
"register" in {
|
||||
val hub = new NumberPoolHub(new LimitedNumberSource(51))
|
||||
val pool = hub.AddPool("test", (25 to 50).toList)
|
||||
pool.Selector = new RandomSelector
|
||||
val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor")
|
||||
val poolAccessor = system.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool, poolActor), name = "accessor")
|
||||
|
||||
val obj : TestEntity = new TestEntity
|
||||
val probe = new TestProbe(system)
|
||||
poolAccessor ! Register(obj, probe.ref)
|
||||
probe.expectMsg(Success(obj))
|
||||
assert({obj.GUID; true}) //NoGUIDException if failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,180 +9,6 @@ class NumberSourceTest extends Specification {
|
|||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
private class TestClass extends IdentifiableEntity
|
||||
|
||||
"MaxNumberSource" should {
|
||||
import net.psforever.objects.guid.source.MaxNumberSource
|
||||
"construct" in {
|
||||
val obj = MaxNumberSource()
|
||||
obj.Size mustEqual Int.MaxValue
|
||||
obj.CountAvailable mustEqual Int.MaxValue
|
||||
obj.CountUsed mustEqual 0
|
||||
}
|
||||
|
||||
"get a number" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result.get.Object mustEqual None
|
||||
obj.Size mustEqual Int.MaxValue
|
||||
obj.CountAvailable mustEqual Int.MaxValue - 1
|
||||
obj.CountUsed mustEqual 1
|
||||
}
|
||||
|
||||
"assign the number" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.Object = new TestClass()
|
||||
ok
|
||||
}
|
||||
|
||||
"return a number (unused)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
obj.CountUsed mustEqual 1
|
||||
val ret = obj.Return(result.get)
|
||||
ret mustEqual None
|
||||
obj.CountUsed mustEqual 0
|
||||
}
|
||||
|
||||
"return a number (assigned)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test = new TestClass()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Object = test
|
||||
obj.CountUsed mustEqual 1
|
||||
val ret = obj.Return(result.get)
|
||||
ret mustEqual Some(test)
|
||||
obj.CountUsed mustEqual 0
|
||||
}
|
||||
|
||||
"restrict a number (unassigned)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Restrict(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Restricted
|
||||
result.get.Object mustEqual None
|
||||
}
|
||||
|
||||
"restrict a number (assigned + multiple assignments)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test1 = new TestClass()
|
||||
val test2 = new TestClass()
|
||||
val result : Option[LoanedKey] = obj.Restrict(5)
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Restricted
|
||||
result.get.Object mustEqual None
|
||||
result.get.Object = None //assignment 1
|
||||
result.get.Object mustEqual None //still unassigned
|
||||
result.get.Object = test1 //assignment 2
|
||||
result.get.Object mustEqual Some(test1)
|
||||
result.get.Object = test2 //assignment 3
|
||||
result.get.Object mustEqual Some(test1) //same as above
|
||||
}
|
||||
|
||||
"return a restricted number (correctly fail)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test = new TestClass()
|
||||
val result : Option[LoanedKey] = obj.Restrict(5)
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Restricted
|
||||
result.get.Object = test
|
||||
|
||||
obj.Return(5)
|
||||
val result2 : Option[SecureKey] = obj.Get(5)
|
||||
result2.get.GUID mustEqual 5
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Restricted
|
||||
result2.get.Object mustEqual Some(test)
|
||||
}
|
||||
|
||||
"restrict a previously-assigned number" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test = new TestClass()
|
||||
val result1 : Option[LoanedKey] = obj.Available(5)
|
||||
result1.isDefined mustEqual true
|
||||
result1.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result1.get.Object = test
|
||||
val result2 : Option[LoanedKey] = obj.Restrict(5)
|
||||
result2.isDefined mustEqual true
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Restricted
|
||||
result2.get.Object mustEqual Some(test)
|
||||
}
|
||||
|
||||
"check a number (not previously gotten)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result2 : Option[SecureKey] = obj.Get(5)
|
||||
result2.get.GUID mustEqual 5
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Available
|
||||
result2.get.Object mustEqual None
|
||||
}
|
||||
|
||||
"check a number (previously gotten)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result.get.Object mustEqual None
|
||||
val result2 : Option[SecureKey] = obj.Get(5)
|
||||
result2.get.GUID mustEqual 5
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result2.get.Object mustEqual None
|
||||
}
|
||||
|
||||
"check a number (assigned)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.isDefined mustEqual true
|
||||
result.get.GUID mustEqual 5
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result.get.Object = new TestClass()
|
||||
val result2 : Option[SecureKey] = obj.Get(5)
|
||||
result2.get.GUID mustEqual 5
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result2.get.Object mustEqual result.get.Object
|
||||
}
|
||||
|
||||
"check a number (assigned and returned)" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test = new TestClass()
|
||||
val result : Option[LoanedKey] = obj.Available(5)
|
||||
result.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result.get.Object = test
|
||||
val result2 : Option[SecureKey] = obj.Get(5)
|
||||
result2.get.Policy mustEqual AvailabilityPolicy.Leased
|
||||
result2.get.Object.get === test
|
||||
obj.Return(5) mustEqual Some(test)
|
||||
val result3 : Option[SecureKey] = obj.Get(5)
|
||||
result3.get.Policy mustEqual AvailabilityPolicy.Available
|
||||
result3.get.Object mustEqual None
|
||||
}
|
||||
|
||||
"clear" in {
|
||||
val obj = MaxNumberSource()
|
||||
val test1 = new TestClass()
|
||||
val test2 = new TestClass()
|
||||
obj.Available(5) //no assignment
|
||||
obj.Available(10).get.Object = test1
|
||||
obj.Available(15).get.Object = test2
|
||||
obj.Restrict(15)
|
||||
obj.Restrict(20).get.Object = test1
|
||||
obj.CountUsed mustEqual 4
|
||||
|
||||
val list : List[IdentifiableEntity] = obj.Clear()
|
||||
obj.CountUsed mustEqual 0
|
||||
list.size mustEqual 3
|
||||
list.count(obj => { obj == test1 }) mustEqual 2
|
||||
list.count(obj => { obj == test2 }) mustEqual 1
|
||||
}
|
||||
}
|
||||
|
||||
"LimitedNumberSource" should {
|
||||
import net.psforever.objects.guid.source.LimitedNumberSource
|
||||
"construct" in {
|
||||
|
|
|
|||
288
common/src/test/scala/objects/UniqueNumberSystemTest.scala
Normal file
288
common/src/test/scala/objects/UniqueNumberSystemTest.scala
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package objects
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
import net.psforever.objects.guid.actor.{NumberPoolActor, Register, UniqueNumberSystem, Unregister}
|
||||
import net.psforever.objects.guid.selector.RandomSelector
|
||||
import net.psforever.objects.guid.source.LimitedNumberSource
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
class AllocateNumberPoolActors extends ActorTest(ActorSystem("test")) {
|
||||
"AllocateNumberPoolActors" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList)
|
||||
guid.AddPool("pool2", (3001 to 4000).toList)
|
||||
guid.AddPool("pool3", (5001 to 6000).toList)
|
||||
val actorMap = UniqueNumberSystemTest.AllocateNumberPoolActors(guid)
|
||||
assert(actorMap.size == 4)
|
||||
assert(actorMap.get("generic").isDefined) //automatically generated
|
||||
assert(actorMap.get("pool1").isDefined)
|
||||
assert(actorMap.get("pool2").isDefined)
|
||||
assert(actorMap.get("pool3").isDefined)
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest extends ActorTest(ActorSystem("test")) {
|
||||
"UniqueNumberSystem" should {
|
||||
"constructor" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList)
|
||||
guid.AddPool("pool2", (3001 to 4000).toList)
|
||||
guid.AddPool("pool3", (5001 to 6000).toList)
|
||||
system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
//as long as it constructs ...
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest1 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Register (success)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
val pool1 = (1001 to 2000).toList
|
||||
val pool2 = (3001 to 4000).toList
|
||||
val pool3 = (5001 to 6000).toList
|
||||
guid.AddPool("pool1", pool1).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", pool2).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", pool3).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
assert(src.CountUsed == 0)
|
||||
//pool1
|
||||
for(_ <- 1 to 100) {
|
||||
val testObj = new EntityTestClass()
|
||||
uns ! Register(testObj, "pool1")
|
||||
val msg = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg.isInstanceOf[Success[_]])
|
||||
assert(pool1.contains(testObj.GUID.guid))
|
||||
}
|
||||
//pool2
|
||||
for(_ <- 1 to 100) {
|
||||
val testObj = new EntityTestClass()
|
||||
uns ! Register(testObj, "pool2")
|
||||
val msg = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg.isInstanceOf[Success[_]])
|
||||
assert(pool2.contains(testObj.GUID.guid))
|
||||
}
|
||||
//pool3
|
||||
for(_ <- 1 to 100) {
|
||||
val testObj = new EntityTestClass()
|
||||
uns ! Register(testObj, "pool3")
|
||||
val msg = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg.isInstanceOf[Success[_]])
|
||||
assert(pool3.contains(testObj.GUID.guid))
|
||||
}
|
||||
assert(src.CountUsed == 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest2 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Register (success; already registered)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Register(testObj, "pool1")
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Success[_]])
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 1)
|
||||
|
||||
val id = testObj.GUID.guid
|
||||
uns ! Register(testObj, "pool2") //different pool; makes no difference
|
||||
val msg2 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg2.isInstanceOf[Success[_]])
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 1)
|
||||
assert(testObj.GUID.guid == id) //unchanged
|
||||
}
|
||||
}
|
||||
//a log.warn should have been generated during this test
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest3 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Register (failure; no pool)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Register(testObj, "pool4")
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Failure[_]])
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest4 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Register (failure; empty pool)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool4", 50 :: Nil).Selector = new RandomSelector //list of one element; can not add an empty list
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
|
||||
val testObj1 = new EntityTestClass()
|
||||
uns ! Register(testObj1, "pool4")
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Success[_]]) //pool4 is now empty
|
||||
|
||||
val testObj2 = new EntityTestClass()
|
||||
uns ! Register(testObj2, "pool4")
|
||||
val msg2 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg2.isInstanceOf[Failure[_]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest5 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Unregister (success)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
val pool2 = (3001 to 4000).toList
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", pool2).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Register(testObj, "pool2")
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Success[_]])
|
||||
assert(testObj.HasGUID)
|
||||
assert(pool2.contains(testObj.GUID.guid))
|
||||
assert(src.CountUsed == 1)
|
||||
|
||||
uns ! Unregister(testObj)
|
||||
val msg2 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg2.isInstanceOf[Success[_]])
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest6 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Unregister (success; object not registered at all)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Unregister(testObj)
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Success[_]])
|
||||
assert(!testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest7 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Unregister (failure; number not in system)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
testObj.GUID = net.psforever.packet.game.PlanetSideGUID(6001) //fake registering; number too high
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Unregister(testObj)
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Failure[_]])
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UniqueNumberSystemTest8 extends ActorTest(ActorSystem("test")) {
|
||||
class EntityTestClass extends IdentifiableEntity
|
||||
|
||||
"UniqueNumberSystem" should {
|
||||
"Unregister (failure; object is not registered to that number)" in {
|
||||
val src : LimitedNumberSource = LimitedNumberSource(6000)
|
||||
val guid : NumberPoolHub = new NumberPoolHub(src)
|
||||
guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
|
||||
guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
|
||||
val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
|
||||
val testObj = new EntityTestClass()
|
||||
testObj.GUID = net.psforever.packet.game.PlanetSideGUID(3500) //fake registering
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
|
||||
uns ! Unregister(testObj)
|
||||
val msg1 = receiveOne(Duration.create(100, "ms"))
|
||||
assert(msg1.isInstanceOf[Failure[_]])
|
||||
assert(testObj.HasGUID)
|
||||
assert(src.CountUsed == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UniqueNumberSystemTest {
|
||||
/**
|
||||
* @see `UniqueNumberSystem.AllocateNumberPoolActors(NumberPoolHub)(implicit ActorContext)`
|
||||
*/
|
||||
def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit system : ActorSystem) : Map[String, ActorRef] = {
|
||||
poolSource.Pools.map({ case ((pname, pool)) =>
|
||||
pname -> system.actorOf(Props(classOf[NumberPoolActor], pool), pname)
|
||||
}).toMap
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
// Copyright (c) 2016 PSForever.net to present
|
||||
import akka.actor.Actor
|
||||
import akka.event.{ActorEventBus, SubchannelClassification}
|
||||
import akka.util.Subclassification
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.types.ExoSuitType
|
||||
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
sealed trait Action
|
||||
|
||||
sealed trait Response
|
||||
|
||||
final case class Join(channel : String)
|
||||
final case class Leave()
|
||||
final case class LeaveAll()
|
||||
|
||||
object AvatarAction {
|
||||
final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action
|
||||
//final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action
|
||||
final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
|
||||
final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action
|
||||
final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
|
||||
// final case class LoadMap(msg : PlanetSideGUID) extends Action
|
||||
// final case class unLoadMap(msg : PlanetSideGUID) extends Action
|
||||
final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
|
||||
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
|
||||
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
||||
final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
|
||||
final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action
|
||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
|
||||
}
|
||||
|
||||
object AvatarServiceResponse {
|
||||
final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response
|
||||
//final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response
|
||||
final case class EquipmentInHand(slot : Int, item : Equipment) extends Response
|
||||
final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response
|
||||
final case class LoadPlayer(pdata : ConstructorData) extends Response
|
||||
// final case class unLoadMap() extends Response
|
||||
// final case class LoadMap() extends Response
|
||||
final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
|
||||
final case class ObjectHeld(slot : Int) extends Response
|
||||
final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response
|
||||
final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
|
||||
final case class Reload(mag : Int) extends Response
|
||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||
// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
|
||||
// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
|
||||
// final case class ChangeWeapon(facingYaw : Int) extends Response
|
||||
}
|
||||
|
||||
final case class AvatarServiceMessage(forChannel : String, actionMessage : Action)
|
||||
|
||||
final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response)
|
||||
|
||||
/*
|
||||
/avatar/
|
||||
*/
|
||||
|
||||
class AvatarEventBus extends ActorEventBus with SubchannelClassification {
|
||||
type Event = AvatarServiceResponse
|
||||
type Classifier = String
|
||||
|
||||
protected def classify(event: Event): Classifier = event.toChannel
|
||||
|
||||
protected def subclassification = new Subclassification[Classifier] {
|
||||
def isEqual(x: Classifier, y: Classifier) = x == y
|
||||
def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y)
|
||||
}
|
||||
|
||||
protected def publish(event: Event, subscriber: Subscriber): Unit = {
|
||||
subscriber ! event
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarService extends Actor {
|
||||
//import AvatarServiceResponse._
|
||||
private [this] val log = org.log4s.getLogger
|
||||
|
||||
override def preStart = {
|
||||
log.info("Starting...")
|
||||
}
|
||||
|
||||
val AvatarEvents = new AvatarEventBus
|
||||
|
||||
/*val channelMap = Map(
|
||||
AvatarMessageType.CMT_OPEN -> AvatarPath("local")
|
||||
)*/
|
||||
|
||||
def receive = {
|
||||
case Join(channel) =>
|
||||
val path = "/Avatar/" + channel
|
||||
val who = sender()
|
||||
|
||||
log.info(s"$who has joined $path")
|
||||
|
||||
AvatarEvents.subscribe(who, path)
|
||||
case Leave() =>
|
||||
AvatarEvents.unsubscribe(sender())
|
||||
case LeaveAll() =>
|
||||
AvatarEvents.unsubscribe(sender())
|
||||
|
||||
case AvatarServiceMessage(forChannel, action) =>
|
||||
action match {
|
||||
case AvatarAction.ArmorChanged(player_guid, suit, subtype) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype))
|
||||
)
|
||||
case AvatarAction.EquipmentInHand(player_guid, slot, obj) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj))
|
||||
)
|
||||
case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj))
|
||||
)
|
||||
case AvatarAction.LoadPlayer(player_guid, pdata) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.LoadPlayer(pdata))
|
||||
)
|
||||
case AvatarAction.ObjectDelete(player_guid, item_guid, unk) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk))
|
||||
)
|
||||
case AvatarAction.ObjectHeld(player_guid, slot) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectHeld(slot))
|
||||
)
|
||||
case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value))
|
||||
)
|
||||
case AvatarAction.PlayerState(guid, msg, spectator, weapon) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon))
|
||||
)
|
||||
case AvatarAction.Reload(player_guid, mag) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag))
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
/*
|
||||
case AvatarService.PlayerStateMessage(msg) =>
|
||||
// log.info(s"NEW: ${m}")
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
|
||||
AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
|
||||
))
|
||||
|
||||
}
|
||||
case AvatarService.LoadMap(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.LoadMap()
|
||||
))
|
||||
}
|
||||
case AvatarService.unLoadMap(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.unLoadMap()
|
||||
))
|
||||
}
|
||||
case AvatarService.ObjectHeld(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.ObjectHeld()
|
||||
))
|
||||
}
|
||||
case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
|
||||
AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
|
||||
))
|
||||
}
|
||||
case AvatarService.PlayerStateShift(killer, guid) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
|
||||
AvatarServiceReply.PlayerStateShift(killer)
|
||||
))
|
||||
}
|
||||
case AvatarService.DestroyDisplay(killer, victim) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
|
||||
AvatarServiceReply.DestroyDisplay(killer)
|
||||
))
|
||||
}
|
||||
case AvatarService.HitHintReturn(source_guid,victim_guid) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
|
||||
AvatarServiceReply.DestroyDisplay(source_guid)
|
||||
))
|
||||
}
|
||||
case AvatarService.ChangeWeapon(unk1, sessionId) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
|
||||
AvatarServiceReply.ChangeWeapon(unk1)
|
||||
))
|
||||
}
|
||||
*/
|
||||
case msg =>
|
||||
log.info(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import java.net.InetAddress
|
|||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.actor.{ActorContext, ActorRef, ActorSystem, Props}
|
||||
import akka.routing.RandomPool
|
||||
import ch.qos.logback.classic.LoggerContext
|
||||
import ch.qos.logback.classic.joran.JoranConfigurator
|
||||
|
|
@ -12,11 +12,14 @@ import ch.qos.logback.core.status._
|
|||
import ch.qos.logback.core.util.StatusPrinter
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.objects.zones.{InterstellarCluster, TerminalObjectBuilder, Zone, ZoneMap}
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.objects.guid.TaskResolver
|
||||
import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockObjectBuilder, TerminalObjectBuilder}
|
||||
import org.slf4j
|
||||
import org.fusesource.jansi.Ansi._
|
||||
import org.fusesource.jansi.Ansi.Color._
|
||||
import services.avatar._
|
||||
import services.local._
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.Await
|
||||
|
|
@ -202,6 +205,7 @@ object PsLogin {
|
|||
val serviceManager = ServiceManager.boot
|
||||
serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver")
|
||||
serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar")
|
||||
serviceManager ! ServiceManager.Register(Props[LocalService], "local")
|
||||
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], createContinents()), "galaxy")
|
||||
|
||||
/** Create two actors for handling the login and world server endpoints */
|
||||
|
|
@ -222,11 +226,38 @@ object PsLogin {
|
|||
def createContinents() : List[Zone] = {
|
||||
val map13 = new ZoneMap("map13") {
|
||||
import net.psforever.objects.GlobalDefinitions._
|
||||
LocalObject(TerminalObjectBuilder(orderTerminal, 853))
|
||||
LocalObject(TerminalObjectBuilder(orderTerminal, 855))
|
||||
LocalObject(TerminalObjectBuilder(orderTerminal, 860))
|
||||
|
||||
LocalObject(DoorObjectBuilder(door, 330))
|
||||
LocalObject(DoorObjectBuilder(door, 332))
|
||||
LocalObject(DoorObjectBuilder(door, 372))
|
||||
LocalObject(DoorObjectBuilder(door, 373))
|
||||
LocalObject(IFFLockObjectBuilder(lock_external, 556))
|
||||
LocalObject(IFFLockObjectBuilder(lock_external, 558))
|
||||
LocalObject(TerminalObjectBuilder(cert_terminal, 186))
|
||||
LocalObject(TerminalObjectBuilder(cert_terminal, 187))
|
||||
LocalObject(TerminalObjectBuilder(cert_terminal, 188))
|
||||
LocalObject(TerminalObjectBuilder(order_terminal, 853))
|
||||
LocalObject(TerminalObjectBuilder(order_terminal, 855))
|
||||
LocalObject(TerminalObjectBuilder(order_terminal, 860))
|
||||
|
||||
LocalBases = 30
|
||||
|
||||
ObjectToBase(330, 29)
|
||||
ObjectToBase(332, 29)
|
||||
ObjectToBase(556, 29)
|
||||
ObjectToBase(558, 29)
|
||||
DoorToLock(330, 558)
|
||||
DoorToLock(332, 556)
|
||||
}
|
||||
val home3 = new Zone("home3", map13, 13) {
|
||||
override def Init(implicit context : ActorContext) : Unit = {
|
||||
super.Init(context)
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
Base(2).get.Faction = PlanetSideEmpire.VS //HART building C
|
||||
Base(29).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower
|
||||
}
|
||||
}
|
||||
val home3 = Zone("home3", map13, 13)
|
||||
|
||||
home3 ::
|
||||
Nil
|
||||
|
|
|
|||
|
|
@ -2,24 +2,30 @@
|
|||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import net.psforever.packet.{PlanetSideGamePacket, _}
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.game.{ObjectCreateDetailedMessage, _}
|
||||
import net.psforever.packet.game._
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
import org.log4s.MDC
|
||||
import MDCContextAware.Implicits._
|
||||
import ServiceManager.Lookup
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.zones.{InterstellarCluster, Zone}
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.guid.{Task, TaskResolver}
|
||||
import net.psforever.objects.guid.actor.{Register, Unregister}
|
||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
||||
import net.psforever.objects.terminals.Terminal
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.locks.IFFLock
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.types._
|
||||
import services._
|
||||
import services.avatar._
|
||||
import services.local._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.Success
|
||||
|
|
@ -31,18 +37,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
var sessionId : Long = 0
|
||||
var leftRef : ActorRef = ActorRef.noSender
|
||||
var rightRef : ActorRef = ActorRef.noSender
|
||||
var avatarService = Actor.noSender
|
||||
var taskResolver = Actor.noSender
|
||||
var galaxy = Actor.noSender
|
||||
var avatarService : ActorRef = ActorRef.noSender
|
||||
var localService : ActorRef = ActorRef.noSender
|
||||
var taskResolver : ActorRef = Actor.noSender
|
||||
var galaxy : ActorRef = Actor.noSender
|
||||
var continent : Zone = null
|
||||
var progressBarValue : Option[Float] = None
|
||||
|
||||
var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable
|
||||
var progressBarUpdate : Cancellable = WorldSessionActor.DefaultCancellable
|
||||
|
||||
override def postStop() = {
|
||||
if(clientKeepAlive != null)
|
||||
clientKeepAlive.cancel()
|
||||
|
||||
avatarService ! Leave()
|
||||
avatarService ! Service.Leave()
|
||||
localService ! Service.Leave()
|
||||
LivePlayerList.Remove(sessionId) match {
|
||||
case Some(tplayer) =>
|
||||
if(tplayer.HasGUID) {
|
||||
|
|
@ -64,11 +74,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
if(pipe.hasNext) {
|
||||
rightRef = pipe.next
|
||||
rightRef !> HelloFriend(sessionId, pipe)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
rightRef = sender()
|
||||
}
|
||||
context.become(Started)
|
||||
ServiceManager.serviceManager ! Lookup("avatar")
|
||||
ServiceManager.serviceManager ! Lookup("local")
|
||||
ServiceManager.serviceManager ! Lookup("taskResolver")
|
||||
ServiceManager.serviceManager ! Lookup("galaxy")
|
||||
|
||||
|
|
@ -81,6 +93,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
case ServiceManager.LookupResult("avatar", endpoint) =>
|
||||
avatarService = endpoint
|
||||
log.info("ID: " + sessionId + " Got avatar service " + endpoint)
|
||||
case ServiceManager.LookupResult("local", endpoint) =>
|
||||
localService = endpoint
|
||||
log.info("ID: " + sessionId + " Got local service " + endpoint)
|
||||
case ServiceManager.LookupResult("taskResolver", endpoint) =>
|
||||
taskResolver = endpoint
|
||||
log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
|
||||
|
|
@ -206,6 +221,48 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
case _ => ;
|
||||
}
|
||||
|
||||
case LocalServiceResponse(_, guid, reply) =>
|
||||
reply match {
|
||||
case LocalServiceResponse.DoorOpens(door_guid) =>
|
||||
if(player.GUID != guid) {
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16)))
|
||||
}
|
||||
|
||||
case LocalServiceResponse.DoorCloses(door_guid) => //door closes for everyone
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17)))
|
||||
|
||||
case LocalServiceResponse.HackClear(target_guid, unk1, unk2) =>
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)))
|
||||
|
||||
case LocalServiceResponse.HackObject(target_guid, unk1, unk2) =>
|
||||
if(player.GUID != guid) {
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)))
|
||||
}
|
||||
|
||||
case LocalServiceResponse.TriggerSound(sound, pos, unk, volume) =>
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, TriggerSoundMessage(sound, pos, unk, volume)))
|
||||
}
|
||||
|
||||
case Door.DoorMessage(tplayer, msg, order) =>
|
||||
val door_guid = msg.object_guid
|
||||
order match {
|
||||
case Door.OpenEvent() =>
|
||||
continent.GUID(door_guid) match {
|
||||
case Some(door : Door) =>
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16)))
|
||||
localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) )
|
||||
|
||||
case _ =>
|
||||
log.warn(s"door $door_guid wanted to be opened but could not be found")
|
||||
}
|
||||
|
||||
case Door.CloseEvent() =>
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17)))
|
||||
localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid))
|
||||
|
||||
case Door.NoEvent() => ;
|
||||
}
|
||||
|
||||
case Terminal.TerminalMessage(tplayer, msg, order) =>
|
||||
order match {
|
||||
case Terminal.BuyExosuit(exosuit, subtype) =>
|
||||
|
|
@ -312,6 +369,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj))
|
||||
})
|
||||
}
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)))
|
||||
|
||||
case Terminal.BuyEquipment(item) => ;
|
||||
tplayer.Fit(item) match {
|
||||
|
|
@ -382,6 +440,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
PutEquipmentInSlot(tplayer, entry.obj, entry.start)
|
||||
})
|
||||
//TODO drop items on ground
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)))
|
||||
|
||||
case Terminal.LearnCertification(cert, cost) =>
|
||||
if(!player.Certifications.contains(cert)) {
|
||||
log.info(s"$tplayer is learning the $cert certification for $cost points")
|
||||
tplayer.Certifications += cert
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id.toLong)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)))
|
||||
}
|
||||
else {
|
||||
log.warn(s"$tplayer already knows the $cert certification, so he can't learn it")
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)))
|
||||
}
|
||||
|
||||
case Terminal.SellCertification(cert, cost) =>
|
||||
if(player.Certifications.contains(cert)) {
|
||||
log.info(s"$tplayer is forgetting the $cert certification for $cost points")
|
||||
tplayer.Certifications -= cert
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id.toLong)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)))
|
||||
}
|
||||
else {
|
||||
log.warn(s"$tplayer doesn't know what a $cert certification is, so he can't forget it")
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)))
|
||||
}
|
||||
|
||||
case Terminal.NoDeal() =>
|
||||
log.warn(s"$tplayer made a request but the terminal rejected the order $msg")
|
||||
|
|
@ -389,7 +472,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
|
||||
case ListAccountCharacters =>
|
||||
import net.psforever.objects.definition.converter.CharacterSelectConverter
|
||||
val gen : AtomicInteger = new AtomicInteger(1)
|
||||
val converter : CharacterSelectConverter = new CharacterSelectConverter
|
||||
|
||||
//load characters
|
||||
SetCharacterSelectScreenGUID(player, gen)
|
||||
|
|
@ -398,7 +483,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
val armor = player.Armor
|
||||
player.Spawn
|
||||
sendResponse(PacketCoding.CreateGamePacket(0,
|
||||
ObjectCreateMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.ConstructorData(player).get)
|
||||
ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, converter.DetailedConstructorData(player).get)
|
||||
))
|
||||
if(health > 0) { //player can not be dead; stay spawned as alive
|
||||
player.Health = health
|
||||
|
|
@ -477,6 +562,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
LivePlayerList.Assign(continent.Number, sessionId, guid)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None))) //CC on
|
||||
|
||||
case Zone.ItemFromGround(tplayer, item) =>
|
||||
val obj_guid = item.GUID
|
||||
|
|
@ -503,6 +589,35 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore
|
||||
}
|
||||
|
||||
case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) =>
|
||||
progressBarUpdate.cancel
|
||||
if(progressBarValue.isDefined) {
|
||||
val progressBarVal : Float = progressBarValue.get + delta
|
||||
val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility
|
||||
HackState.Start
|
||||
}
|
||||
else if(progressBarVal > 100L) {
|
||||
HackState.Finished
|
||||
}
|
||||
else {
|
||||
HackState.Ongoing
|
||||
}
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)))
|
||||
if(progressBarVal > 100) { //done
|
||||
progressBarValue = None
|
||||
log.info(s"Hacked a $target")
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)))
|
||||
completeAction()
|
||||
}
|
||||
else { //continue next tick
|
||||
tickAction.getOrElse(() => Unit)()
|
||||
progressBarValue = Some(progressBarVal)
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction))
|
||||
}
|
||||
}
|
||||
|
||||
case ResponseToSelf(pkt) =>
|
||||
log.info(s"Received a direct message: $pkt")
|
||||
sendResponse(pkt)
|
||||
|
|
@ -625,6 +740,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
log.info(s"New world login to $server with Token:$token. $clientVersion")
|
||||
self ! ListAccountCharacters
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clientKeepAlive.cancel
|
||||
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
|
||||
|
||||
case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
|
||||
log.info("Handling " + msg)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None)))
|
||||
|
|
@ -641,11 +761,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
//TODO if yes, get continent guid accessors
|
||||
//TODO if no, get sanctuary guid accessors and reset the player's expectations
|
||||
galaxy ! InterstellarCluster.GetWorld("home3")
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clientKeepAlive.cancel
|
||||
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
|
||||
case default =>
|
||||
log.error("Unsupported " + default + " in " + msg)
|
||||
}
|
||||
|
|
@ -672,7 +787,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
)
|
||||
})
|
||||
//render Equipment that was dropped into zone before the player arrived
|
||||
continent.EquipmentOnGround.toList.foreach(item => {
|
||||
continent.EquipmentOnGround.foreach(item => {
|
||||
val definition = item.Definition
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(0,
|
||||
|
|
@ -685,7 +800,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
)
|
||||
})
|
||||
|
||||
avatarService ! Join(player.Continent)
|
||||
avatarService ! Service.Join(player.Continent)
|
||||
localService ! Service.Join(player.Continent)
|
||||
self ! SetCurrentAvatar(player)
|
||||
|
||||
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
|
||||
|
|
@ -747,6 +863,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
|
||||
case msg @ ChangeFireStateMessage_Stop(item_guid) =>
|
||||
log.info("ChangeFireState_Stop: " + msg)
|
||||
progressBarUpdate.cancel
|
||||
|
||||
case msg @ EmoteMsg(avatar_guid, emote) =>
|
||||
log.info("Emote: " + msg)
|
||||
|
|
@ -916,15 +1033,53 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
log.info("UseItem: " + msg)
|
||||
// TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)
|
||||
// TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg)
|
||||
if (itemType != 121) sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
|
||||
if (itemType == 121 && !unk3){ // TODO : medkit use ?!
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2)))
|
||||
}
|
||||
if (unk1 == 0 && !unk3 && unk7 == 25) {
|
||||
// TODO: This should only actually be sent to doors upon opening; may break non-door items upon use
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16)))
|
||||
continent.GUID(object_guid) match {
|
||||
case Some(door : Door) =>
|
||||
continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFF Lock
|
||||
case Some(lock_guid) =>
|
||||
val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy match {
|
||||
case Some((tplayer, _, _)) =>
|
||||
tplayer.Faction == player.Faction
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
continent.Map.ObjectToBase.get(lock_guid) match { //check for associated base
|
||||
case Some(base_id) =>
|
||||
if(continent.Base(base_id).get.Faction == player.Faction || lock_hacked) { //either base allegiance aligns or locks is hacked
|
||||
door.Actor ! Door.Use(player, msg)
|
||||
}
|
||||
case None =>
|
||||
if(lock_hacked) { //is lock hacked? this may be a weird case
|
||||
door.Actor ! Door.Use(player, msg)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
door.Actor ! Door.Use(player, msg) //let door open freely
|
||||
}
|
||||
|
||||
case Some(panel : IFFLock) =>
|
||||
player.Slot(player.DrawnSlot).Equipment match {
|
||||
case Some(tool : SimpleItem) =>
|
||||
if(tool.Definition == GlobalDefinitions.remote_electronics_kit) {
|
||||
//TODO get player hack level (for now, presume 15s in intervals of 4/s)
|
||||
progressBarValue = Some(-2.66f)
|
||||
self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, FinishHackingDoor(panel, 1114636288L))
|
||||
log.info("Hacking a door~")
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case Some(obj : PlanetSideGameObject) =>
|
||||
if(itemType != 121) {
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
|
||||
}
|
||||
else if(itemType == 121 && !unk3) { // TODO : medkit use ?!
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2)))
|
||||
}
|
||||
|
||||
case None => ;
|
||||
}
|
||||
|
||||
case msg @ UnuseItemMessage(player_guid, item) =>
|
||||
|
|
@ -1030,6 +1185,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
case msg @ TargetingImplantRequest(list) =>
|
||||
log.info("TargetingImplantRequest: "+msg)
|
||||
|
||||
case msg @ ActionCancelMessage(u1, u2, u3) =>
|
||||
log.info("Cancelled: "+msg)
|
||||
|
||||
case default => log.error(s"Unhandled GamePacket $pkt")
|
||||
}
|
||||
|
||||
|
|
@ -1175,7 +1333,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
|
||||
def Execute(resolver : ActorRef) : Unit = {
|
||||
localAccessor ! Register(localObject, resolver)
|
||||
localAccessor ! Register(localObject, "dynamic", resolver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1471,6 +1629,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The process of hacking the `Door` `IFFLock` is completed.
|
||||
* Pass the message onto the lock and onto the local events system.
|
||||
* @param target the `IFFLock` belonging to the door that is being hacked
|
||||
* @param unk na;
|
||||
* used by `HackingMessage` as `unk5`
|
||||
* @see `HackMessage`
|
||||
*/
|
||||
//TODO add params here depending on which params in HackMessage are important
|
||||
//TODO sound should be centered on IFFLock, not on player
|
||||
private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = {
|
||||
target.Actor ! CommonMessages.Hack(player)
|
||||
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackDoor, player.Position, 30, 0.49803925f))
|
||||
localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk))
|
||||
}
|
||||
|
||||
def failWithError(error : String) = {
|
||||
log.error(error)
|
||||
sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
||||
|
|
@ -1502,6 +1676,23 @@ object WorldSessionActor {
|
|||
private final case class ListAccountCharacters()
|
||||
private final case class SetCurrentAvatar(tplayer : Player)
|
||||
|
||||
/**
|
||||
* A message that indicates the user is using a remote electronics kit to hack some server object.
|
||||
* Each time this message is sent for a given hack attempt counts as a single "tick" of progress.
|
||||
* The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more.
|
||||
* @param tplayer the player
|
||||
* @param target the object being hacked
|
||||
* @param tool_guid the REK
|
||||
* @param delta how much the progress bar value changes each tick
|
||||
* @param completeAction a custom action performed once the hack is completed
|
||||
* @param tickAction an optional action is is performed for each tick of progress
|
||||
*/
|
||||
private final case class ItemHacking(tplayer : Player,
|
||||
target : PlanetSideServerObject,
|
||||
tool_guid : PlanetSideGUID,
|
||||
delta : Float,
|
||||
completeAction : () => Unit,
|
||||
tickAction : Option[() => Unit] = None)
|
||||
/**
|
||||
* A placeholder `Cancellable` object.
|
||||
*/
|
||||
|
|
|
|||
34
pslogin/src/main/scala/services/Service.scala
Normal file
34
pslogin/src/main/scala/services/Service.scala
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services
|
||||
|
||||
import akka.event.{ActorEventBus, SubchannelClassification}
|
||||
import akka.util.Subclassification
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
|
||||
object Service {
|
||||
final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0)
|
||||
|
||||
final case class Join(channel : String)
|
||||
final case class Leave()
|
||||
final case class LeaveAll()
|
||||
}
|
||||
|
||||
trait GenericEventBusMsg {
|
||||
def toChannel : String
|
||||
}
|
||||
|
||||
class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification {
|
||||
type Event = A
|
||||
type Classifier = String
|
||||
|
||||
protected def classify(event: Event): Classifier = event.toChannel
|
||||
|
||||
protected def subclassification = new Subclassification[Classifier] {
|
||||
def isEqual(x: Classifier, y: Classifier) = x == y
|
||||
def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y)
|
||||
}
|
||||
|
||||
protected def publish(event: Event, subscriber: Subscriber): Unit = {
|
||||
subscriber ! event
|
||||
}
|
||||
}
|
||||
28
pslogin/src/main/scala/services/avatar/AvatarAction.scala
Normal file
28
pslogin/src/main/scala/services/avatar/AvatarAction.scala
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.avatar
|
||||
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.types.{ExoSuitType, Vector3}
|
||||
|
||||
object AvatarAction {
|
||||
trait Action
|
||||
|
||||
final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action
|
||||
//final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action
|
||||
final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
|
||||
final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action
|
||||
final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
|
||||
// final case class LoadMap(msg : PlanetSideGUID) extends Action
|
||||
// final case class unLoadMap(msg : PlanetSideGUID) extends Action
|
||||
final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
|
||||
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
|
||||
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
|
||||
final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
|
||||
final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action
|
||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
|
||||
}
|
||||
150
pslogin/src/main/scala/services/avatar/AvatarService.scala
Normal file
150
pslogin/src/main/scala/services/avatar/AvatarService.scala
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.avatar
|
||||
|
||||
import akka.actor.Actor
|
||||
import services.{GenericEventBus, Service}
|
||||
|
||||
class AvatarService extends Actor {
|
||||
//import AvatarServiceResponse._
|
||||
private [this] val log = org.log4s.getLogger
|
||||
|
||||
override def preStart = {
|
||||
log.info("Starting...")
|
||||
}
|
||||
|
||||
val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus
|
||||
|
||||
def receive = {
|
||||
case Service.Join(channel) =>
|
||||
val path = s"/$channel/Avatar"
|
||||
val who = sender()
|
||||
|
||||
log.info(s"$who has joined $path")
|
||||
|
||||
AvatarEvents.subscribe(who, path)
|
||||
case Service.Leave() =>
|
||||
AvatarEvents.unsubscribe(sender())
|
||||
case Service.LeaveAll() =>
|
||||
AvatarEvents.unsubscribe(sender())
|
||||
|
||||
case AvatarServiceMessage(forChannel, action) =>
|
||||
action match {
|
||||
case AvatarAction.ArmorChanged(player_guid, suit, subtype) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype))
|
||||
)
|
||||
case AvatarAction.EquipmentInHand(player_guid, slot, obj) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj))
|
||||
)
|
||||
case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj))
|
||||
)
|
||||
case AvatarAction.LoadPlayer(player_guid, pdata) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata))
|
||||
)
|
||||
case AvatarAction.ObjectDelete(player_guid, item_guid, unk) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk))
|
||||
)
|
||||
case AvatarAction.ObjectHeld(player_guid, slot) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectHeld(slot))
|
||||
)
|
||||
case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value))
|
||||
)
|
||||
case AvatarAction.PlayerState(guid, msg, spectator, weapon) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon))
|
||||
)
|
||||
case AvatarAction.Reload(player_guid, mag) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Reload(mag))
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
/*
|
||||
case AvatarService.PlayerStateMessage(msg) =>
|
||||
// log.info(s"NEW: ${m}")
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
|
||||
AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
|
||||
))
|
||||
|
||||
}
|
||||
case AvatarService.LoadMap(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.LoadMap()
|
||||
))
|
||||
}
|
||||
case AvatarService.unLoadMap(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.unLoadMap()
|
||||
))
|
||||
}
|
||||
case AvatarService.ObjectHeld(msg) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
|
||||
AvatarServiceReply.ObjectHeld()
|
||||
))
|
||||
}
|
||||
case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
|
||||
AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
|
||||
))
|
||||
}
|
||||
case AvatarService.PlayerStateShift(killer, guid) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
|
||||
AvatarServiceReply.PlayerStateShift(killer)
|
||||
))
|
||||
}
|
||||
case AvatarService.DestroyDisplay(killer, victim) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
|
||||
AvatarServiceReply.DestroyDisplay(killer)
|
||||
))
|
||||
}
|
||||
case AvatarService.HitHintReturn(source_guid,victim_guid) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
|
||||
AvatarServiceReply.DestroyDisplay(source_guid)
|
||||
))
|
||||
}
|
||||
case AvatarService.ChangeWeapon(unk1, sessionId) =>
|
||||
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
|
||||
if (playerOpt.isDefined) {
|
||||
val player: PlayerAvatar = playerOpt.get
|
||||
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
|
||||
AvatarServiceReply.ChangeWeapon(unk1)
|
||||
))
|
||||
}
|
||||
*/
|
||||
case msg =>
|
||||
log.info(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.avatar
|
||||
|
||||
final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.avatar
|
||||
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.types.{ExoSuitType, Vector3}
|
||||
import services.GenericEventBusMsg
|
||||
|
||||
final case class AvatarServiceResponse(toChannel : String,
|
||||
avatar_guid : PlanetSideGUID,
|
||||
replyMessage : AvatarServiceResponse.Response
|
||||
) extends GenericEventBusMsg
|
||||
|
||||
object AvatarServiceResponse {
|
||||
trait Response
|
||||
|
||||
final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response
|
||||
//final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response
|
||||
final case class EquipmentInHand(slot : Int, item : Equipment) extends Response
|
||||
final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response
|
||||
final case class LoadPlayer(pdata : ConstructorData) extends Response
|
||||
// final case class unLoadMap() extends Response
|
||||
// final case class LoadMap() extends Response
|
||||
final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
|
||||
final case class ObjectHeld(slot : Int) extends Response
|
||||
final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response
|
||||
final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
|
||||
final case class Reload(mag : Int) extends Response
|
||||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||
// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
|
||||
// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
|
||||
// final case class ChangeWeapon(facingYaw : Int) extends Response
|
||||
}
|
||||
18
pslogin/src/main/scala/services/local/LocalAction.scala
Normal file
18
pslogin/src/main/scala/services/local/LocalAction.scala
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local
|
||||
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
object LocalAction {
|
||||
trait Action
|
||||
|
||||
final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action
|
||||
final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action
|
||||
final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
|
||||
final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
|
||||
final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action
|
||||
}
|
||||
73
pslogin/src/main/scala/services/local/LocalService.scala
Normal file
73
pslogin/src/main/scala/services/local/LocalService.scala
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local
|
||||
|
||||
import akka.actor.{Actor, Props}
|
||||
import services.local.support.{DoorCloseActor, HackClearActor}
|
||||
import services.{GenericEventBus, Service}
|
||||
|
||||
class LocalService extends Actor {
|
||||
//import LocalService._
|
||||
private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer")
|
||||
private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer")
|
||||
private [this] val log = org.log4s.getLogger
|
||||
|
||||
override def preStart = {
|
||||
log.info("Starting...")
|
||||
}
|
||||
|
||||
val LocalEvents = new GenericEventBus[LocalServiceResponse]
|
||||
|
||||
def receive = {
|
||||
case Service.Join(channel) =>
|
||||
val path = s"/$channel/LocalEnvironment"
|
||||
val who = sender()
|
||||
log.info(s"$who has joined $path")
|
||||
LocalEvents.subscribe(who, path)
|
||||
case Service.Leave() =>
|
||||
LocalEvents.unsubscribe(sender())
|
||||
case Service.LeaveAll() =>
|
||||
LocalEvents.unsubscribe(sender())
|
||||
|
||||
case LocalServiceMessage(forChannel, action) =>
|
||||
action match {
|
||||
case LocalAction.DoorOpens(player_guid, zone, door) =>
|
||||
doorCloser ! DoorCloseActor.DoorIsOpen(door, zone)
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorOpens(door.GUID))
|
||||
)
|
||||
case LocalAction.DoorCloses(player_guid, door_guid) =>
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorCloses(door_guid))
|
||||
)
|
||||
case LocalAction.HackClear(player_guid, target, unk1, unk2) =>
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackClear(target.GUID, unk1, unk2))
|
||||
)
|
||||
case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) =>
|
||||
hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2)
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2))
|
||||
)
|
||||
case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) =>
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.TriggerSound(sound, pos, unk, volume))
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
//response from DoorCloseActor
|
||||
case DoorCloseActor.CloseTheDoor(door_guid, zone_id) =>
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid))
|
||||
)
|
||||
|
||||
//response from HackClearActor
|
||||
case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) =>
|
||||
LocalEvents.publish(
|
||||
LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2))
|
||||
)
|
||||
|
||||
case msg =>
|
||||
log.info(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local
|
||||
|
||||
final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local
|
||||
|
||||
import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
|
||||
import net.psforever.types.Vector3
|
||||
import services.GenericEventBusMsg
|
||||
|
||||
final case class LocalServiceResponse(toChannel : String,
|
||||
avatar_guid : PlanetSideGUID,
|
||||
replyMessage : LocalServiceResponse.Response
|
||||
) extends GenericEventBusMsg
|
||||
|
||||
object LocalServiceResponse {
|
||||
trait Response
|
||||
|
||||
final case class DoorOpens(door_guid : PlanetSideGUID) extends Response
|
||||
final case class DoorCloses(door_guid : PlanetSideGUID) extends Response
|
||||
final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response
|
||||
final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response
|
||||
final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local.support
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Close an opened door after a certain amount of time has passed.
|
||||
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding doors opening.
|
||||
* @see `LocalService`
|
||||
*/
|
||||
class DoorCloseActor() extends Actor {
|
||||
/** The periodic `Executor` that checks for doors to be closed */
|
||||
private var doorCloserTrigger : Cancellable = DoorCloseActor.DefaultCloser
|
||||
/** A `List` of currently open doors */
|
||||
private var openDoors : List[DoorCloseActor.DoorEntry] = Nil
|
||||
//private[this] val log = org.log4s.getLogger
|
||||
|
||||
def receive : Receive = {
|
||||
case DoorCloseActor.DoorIsOpen(door, zone, time) =>
|
||||
openDoors = openDoors :+ DoorCloseActor.DoorEntry(door, zone, time)
|
||||
if(openDoors.size == 1) { //we were the only entry so the event must be started from scratch
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
doorCloserTrigger = context.system.scheduler.scheduleOnce(DoorCloseActor.timeout, self, DoorCloseActor.TryCloseDoors())
|
||||
}
|
||||
|
||||
case DoorCloseActor.TryCloseDoors() =>
|
||||
doorCloserTrigger.cancel
|
||||
val now : Long = System.nanoTime
|
||||
val (doorsToClose, doorsLeftOpen) = PartitionEntries(openDoors, now)
|
||||
openDoors = doorsLeftOpen
|
||||
doorsToClose.foreach(entry => {
|
||||
entry.door.Open = false //permissible break from synchronization
|
||||
context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) //call up to the main event system
|
||||
})
|
||||
|
||||
if(doorsLeftOpen.nonEmpty) {
|
||||
val short_timeout : FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - doorsLeftOpen.head.time)) nanoseconds
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
|
||||
* Separate the original `List` into two:
|
||||
* a `List` of elements that have exceeded the time limit,
|
||||
* and a `List` of elements that still satisfy the time limit.
|
||||
* As newer entries to the `List` will always resolve later than old ones,
|
||||
* and newer entries are always added to the end of the main `List`,
|
||||
* processing in order is always correct.
|
||||
* @param list the `List` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @see `List.partition`
|
||||
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
|
||||
*/
|
||||
private def PartitionEntries(list : List[DoorCloseActor.DoorEntry], now : Long) : (List[DoorCloseActor.DoorEntry], List[DoorCloseActor.DoorEntry]) = {
|
||||
val n : Int = recursivePartitionEntries(list.iterator, now)
|
||||
(list.take(n), list.drop(n)) //take and drop so to always return new lists
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the index where the `List` of elements can be divided into two:
|
||||
* a `List` of elements that have exceeded the time limit,
|
||||
* and a `List` of elements that still satisfy the time limit.
|
||||
* @param iter the `Iterator` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @param index a persistent record of the index where list division should occur;
|
||||
* defaults to 0
|
||||
* @return the index where division will occur
|
||||
*/
|
||||
@tailrec private def recursivePartitionEntries(iter : Iterator[DoorCloseActor.DoorEntry], now : Long, index : Int = 0) : Int = {
|
||||
if(!iter.hasNext) {
|
||||
index
|
||||
}
|
||||
else {
|
||||
val entry = iter.next()
|
||||
if(now - entry.time >= DoorCloseActor.timeout_time) {
|
||||
recursivePartitionEntries(iter, now, index + 1)
|
||||
}
|
||||
else {
|
||||
index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DoorCloseActor {
|
||||
/** The wait before an open door closes; as a Long for calculation simplicity */
|
||||
private final val timeout_time : Long = 5000000000L //nanoseconds (5s)
|
||||
/** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */
|
||||
private final val timeout : FiniteDuration = timeout_time nanoseconds
|
||||
|
||||
private final val DefaultCloser : Cancellable = new Cancellable() {
|
||||
override def cancel : Boolean = true
|
||||
override def isCancelled : Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Message that carries information about a door that has been opened.
|
||||
* @param door the door object
|
||||
* @param zone the zone in which the door resides
|
||||
* @param time when the door was opened
|
||||
* @see `DoorEntry`
|
||||
*/
|
||||
final case class DoorIsOpen(door : Door, zone : Zone, time : Long = System.nanoTime())
|
||||
/**
|
||||
* Message that carries information about a door that needs to close.
|
||||
* Prompting, as compared to `DoorIsOpen` which is reactionary.
|
||||
* @param door_guid the door
|
||||
* @param zone_id the zone in which the door resides
|
||||
*/
|
||||
final case class CloseTheDoor(door_guid : PlanetSideGUID, zone_id : String)
|
||||
/**
|
||||
* Internal message used to signal a test of the queued door information.
|
||||
*/
|
||||
private final case class TryCloseDoors()
|
||||
|
||||
/**
|
||||
* Entry of door information.
|
||||
* The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
|
||||
* @param door the door object
|
||||
* @param zone the zone in which the door resides
|
||||
* @param time when the door was opened
|
||||
* @see `DoorIsOpen`
|
||||
*/
|
||||
private final case class DoorEntry(door : Door, zone : Zone, time : Long)
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.local.support
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Restore original functionality to an object that has been hacked after a certain amount of time has passed.
|
||||
* This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding hacking events.
|
||||
* @see `LocalService`
|
||||
*/
|
||||
class HackClearActor() extends Actor {
|
||||
/** The periodic `Executor` that checks for server objects to be unhacked */
|
||||
private var clearTrigger : Cancellable = HackClearActor.DefaultClearer
|
||||
/** A `List` of currently hacked server objects */
|
||||
private var hackedObjects : List[HackClearActor.HackEntry] = Nil
|
||||
//private[this] val log = org.log4s.getLogger
|
||||
|
||||
def receive : Receive = {
|
||||
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, time) =>
|
||||
hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time)
|
||||
if(hackedObjects.size == 1) { //we were the only entry so the event must be started from scratch
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clearTrigger = context.system.scheduler.scheduleOnce(HackClearActor.timeout, self, HackClearActor.TryClearHacks())
|
||||
}
|
||||
|
||||
case HackClearActor.TryClearHacks() =>
|
||||
clearTrigger.cancel
|
||||
val now : Long = System.nanoTime
|
||||
//TODO we can just walk across the list of doors and extract only the first few entries
|
||||
val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
|
||||
hackedObjects = stillHackedObjects
|
||||
unhackObjects.foreach(entry => {
|
||||
entry.target.Actor ! CommonMessages.ClearHack()
|
||||
context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) //call up to the main event system
|
||||
})
|
||||
|
||||
if(stillHackedObjects.nonEmpty) {
|
||||
val short_timeout : FiniteDuration = math.max(1, HackClearActor.timeout_time - (now - stillHackedObjects.head.time)) nanoseconds
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks())
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
|
||||
* Separate the original `List` into two:
|
||||
* a `List` of elements that have exceeded the time limit,
|
||||
* and a `List` of elements that still satisfy the time limit.
|
||||
* As newer entries to the `List` will always resolve later than old ones,
|
||||
* and newer entries are always added to the end of the main `List`,
|
||||
* processing in order is always correct.
|
||||
* @param list the `List` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @see `List.partition`
|
||||
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
|
||||
*/
|
||||
private def PartitionEntries(list : List[HackClearActor.HackEntry], now : Long) : (List[HackClearActor.HackEntry], List[HackClearActor.HackEntry]) = {
|
||||
val n : Int = recursivePartitionEntries(list.iterator, now)
|
||||
(list.take(n), list.drop(n)) //take and drop so to always return new lists
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the index where the `List` of elements can be divided into two:
|
||||
* a `List` of elements that have exceeded the time limit,
|
||||
* and a `List` of elements that still satisfy the time limit.
|
||||
* @param iter the `Iterator` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @param index a persistent record of the index where list division should occur;
|
||||
* defaults to 0
|
||||
* @return the index where division will occur
|
||||
*/
|
||||
@tailrec private def recursivePartitionEntries(iter : Iterator[HackClearActor.HackEntry], now : Long, index : Int = 0) : Int = {
|
||||
if(!iter.hasNext) {
|
||||
index
|
||||
}
|
||||
else {
|
||||
val entry = iter.next()
|
||||
if(now - entry.time >= HackClearActor.timeout_time) {
|
||||
recursivePartitionEntries(iter, now, index + 1)
|
||||
}
|
||||
else {
|
||||
index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object HackClearActor {
|
||||
/** The wait before a server object is to unhack; as a Long for calculation simplicity */
|
||||
private final val timeout_time : Long = 60000000000L //nanoseconds (60s)
|
||||
/** The wait before a server object is to unhack; as a `FiniteDuration` for `Executor` simplicity */
|
||||
private final val timeout : FiniteDuration = timeout_time nanoseconds
|
||||
|
||||
private final val DefaultClearer : Cancellable = new Cancellable() {
|
||||
override def cancel : Boolean = true
|
||||
override def isCancelled : Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Message that carries information about a server object that has been hacked.
|
||||
* @param target the server object
|
||||
* @param zone the zone in which the object resides
|
||||
* @param time when the object was hacked
|
||||
* @see `HackEntry`
|
||||
*/
|
||||
final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long = System.nanoTime())
|
||||
/**
|
||||
* Message that carries information about a server object that needs its functionality restored.
|
||||
* Prompting, as compared to `ObjectIsHacked` which is reactionary.
|
||||
* @param door_guid the server object
|
||||
* @param zone_id the zone in which the object resides
|
||||
*/
|
||||
final case class ClearTheHack(door_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long)
|
||||
/**
|
||||
* Internal message used to signal a test of the queued door information.
|
||||
*/
|
||||
private final case class TryClearHacks()
|
||||
|
||||
/**
|
||||
* Entry of hacked server object information.
|
||||
* The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
|
||||
* @param target the server object
|
||||
* @param zone the zone in which the object resides
|
||||
* @param time when the object was hacked
|
||||
* @see `ObjectIsHacked`
|
||||
*/
|
||||
private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long)
|
||||
}
|
||||
Loading…
Reference in a new issue