Restructure repository

* Move /common/src to /src
* Move services to net.psforever package
* Move /pslogin to /server
This commit is contained in:
Jakob Gillich 2020-08-23 03:26:06 +02:00
parent 89a30ae6f6
commit f4fd78fc5d
958 changed files with 527 additions and 725 deletions

View file

@ -0,0 +1,66 @@
package akka.actor
// Taken from https://medium.com/hootsuite-engineering/logging-contextual-info-in-an-asynchronous-scala-application-8ea33bfec9b3
import akka.util.Timeout
import org.slf4j.MDC
import scala.concurrent.Future
trait MDCContextAware extends Actor with ActorLogging {
import MDCContextAware._
// This is why this needs to be in package akka.actor
override protected[akka] def aroundReceive(receive: Actor.Receive, msg: Any): Unit = {
val orig = MDC.getCopyOfContextMap
try {
msg match {
case mdcObj @ MdcMsg(mdc, origMsg) =>
if (mdc != null)
MDC.setContextMap(mdc)
else
MDC.clear()
super.aroundReceive(receive, origMsg)
case _ =>
super.aroundReceive(receive, msg)
}
} finally {
if (orig != null)
MDC.setContextMap(orig)
else
MDC.clear()
}
}
}
object MDCContextAware {
private case class MdcMsg(mdc: java.util.Map[String, String], msg: Any)
object Implicits {
/**
* Add two new methods that allow MDC info to be passed to MDCContextAware actors.
*
* Do NOT use these methods to send to actors that are not MDCContextAware.
*/
implicit class ContextLocalAwareActorRef(val ref: ActorRef) extends AnyVal {
import akka.pattern.ask
/**
* Send a message to an actor that is MDCContextAware - it will propagate
* the current MDC values. Note: we MUST capture the ActorContext in order for senders
* to be correct! This was a bug from the original author.
*/
def !>(msg: Any)(implicit context: ActorContext): Unit =
ref.tell(MdcMsg(MDC.getCopyOfContextMap, msg), context.self)
/**
* "Ask" an actor that is MDCContextAware for something - it will propagate
* the current MDC values
*/
def ?>(msg: Any)(implicit context: ActorContext, timeout: Timeout): Future[Any] =
ref.ask(MdcMsg(MDC.getCopyOfContextMap, msg), context.self)
}
}
}

View file

@ -0,0 +1,69 @@
package akka.actor
// Taken from https://medium.com/hootsuite-engineering/logging-contextual-info-in-an-asynchronous-scala-application-8ea33bfec9b3
import org.slf4j.MDC
import scala.concurrent.ExecutionContext
trait MDCPropagatingExecutionContext extends ExecutionContext {
// name the self-type "self" so we can refer to it inside the nested class
self =>
override def prepare(): ExecutionContext =
new ExecutionContext {
// Save the call-site MDC state
val context = MDC.getCopyOfContextMap
def execute(r: Runnable): Unit =
self.execute(new Runnable {
def run(): Unit = {
// Save the existing execution-site MDC state
val oldContext = MDC.getCopyOfContextMap
try {
// Set the call-site MDC state into the execution-site MDC
if (context != null)
MDC.setContextMap(context)
else
MDC.clear()
r.run()
} finally {
// Restore the existing execution-site MDC state
if (oldContext != null)
MDC.setContextMap(oldContext)
else
MDC.clear()
}
}
})
def reportFailure(t: Throwable): Unit = self.reportFailure(t)
}
}
object MDCPropagatingExecutionContext {
object Implicits {
// Convenience wrapper around the Scala global ExecutionContext so you can just do:
// import MDCPropagatingExecutionContext.Implicits.global
implicit lazy val global = MDCPropagatingExecutionContextWrapper(ExecutionContext.Implicits.global)
}
}
/**
* Wrapper around an existing ExecutionContext that makes it propagate MDC information.
*/
class MDCPropagatingExecutionContextWrapper(wrapped: ExecutionContext)
extends ExecutionContext
with MDCPropagatingExecutionContext {
override def execute(r: Runnable): Unit = wrapped.execute(r)
override def reportFailure(t: Throwable): Unit = wrapped.reportFailure(t)
}
object MDCPropagatingExecutionContextWrapper {
def apply(wrapped: ExecutionContext): MDCPropagatingExecutionContextWrapper = {
new MDCPropagatingExecutionContextWrapper(wrapped)
}
}

View file

@ -0,0 +1,24 @@
// Copyright (c) 2017 PSForever
package net.psforever
class ObjectFinalizedException(msg: String) extends Exception(msg)
trait IFinalizable {
var closed = false
def close = {
closed = true
}
def assertNotClosed = {
if (closed)
throw new ObjectFinalizedException(
this.getClass.getCanonicalName + ": already finalized. Cannot interact with object"
)
}
override def finalize() = {
if (!closed)
println(this.getClass.getCanonicalName + ": class not closed. memory leaked")
}
}

View file

@ -0,0 +1,29 @@
package net.psforever.actors.commands
import akka.actor.typed.ActorRef
import net.psforever.objects.NtuContainer
object NtuCommand {
trait Command
/** Message for announcing it has nanites it can offer the recipient.
*
* @param source the nanite container recognized as the sender
*/
final case class Offer(source: NtuContainer) extends Command
/** Message for asking for nanites from the recipient.
*
* @param amount the amount of nanites requested
*/
final case class Request(amount: Int, replyTo: ActorRef[Grant]) extends Command
/** Response for transferring nanites to a recipient.
*
* @param source the nanite container recognized as the sender
* @param amount the nanites transferred in this package
*/
final case class Grant(source: NtuContainer, amount: Int)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,934 @@
package net.psforever.actors.session
import akka.actor.Cancellable
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic}
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.{Default, GlobalDefinitions, Player, Session}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
import net.psforever.objects.zones.Zoning
import net.psforever.packet.PacketCoding
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.PointOfInterest
import net.psforever.zones.Zones
import net.psforever.services.chat.ChatService
import net.psforever.services.chat.ChatService.ChatChannel
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration._
object ChatActor {
def apply(
sessionActor: ActorRef[SessionActor.Command],
avatarActor: ActorRef[AvatarActor.Command]
): Behavior[Command] =
Behaviors
.supervise[Command] {
Behaviors.withStash(100) { buffer =>
Behaviors.setup(context => new ChatActor(context, buffer, sessionActor, avatarActor).start())
}
}
.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
final case class JoinChannel(channel: ChatChannel) extends Command
final case class LeaveChannel(channel: ChatChannel) extends Command
final case class Message(message: ChatMsg) extends Command
final case class SetSession(session: Session) extends Command
private case class ListingResponse(listing: Receptionist.Listing) extends Command
private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
}
class ChatActor(
context: ActorContext[ChatActor.Command],
buffer: StashBuffer[ChatActor.Command],
sessionActor: ActorRef[SessionActor.Command],
avatarActor: ActorRef[AvatarActor.Command]
) {
import ChatActor._
implicit val ec: ExecutionContextExecutor = context.executionContext
private[this] val log = org.log4s.getLogger
var channels: List[ChatChannel] = List()
var session: Option[Session] = None
var chatService: Option[ActorRef[ChatService.Command]] = None
var silenceTimer: Cancellable = Default.Cancellable
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
case ChatService.MessageResponse(session, message, channel) => IncomingMessage(session, message, channel)
}
context.system.receptionist ! Receptionist.Find(
ChatService.ChatServiceKey,
context.messageAdapter[Receptionist.Listing](ListingResponse)
)
def start(): Behavior[Command] = {
Behaviors
.receiveMessage[Command] {
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
chatService = Some(listings.head)
channels ++= List(ChatChannel.Default())
postStartBehaviour()
case SetSession(newSession) =>
session = Some(newSession)
postStartBehaviour()
case other =>
buffer.stash(other)
Behaviors.same
}
}
def postStartBehaviour(): Behavior[Command] = {
(session, chatService) match {
case (Some(session), Some(chatService)) if session.player != null =>
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, ChatChannel.Default())
buffer.unstashAll(active(session, chatService))
case _ =>
Behaviors.same
}
}
def active(session: Session, chatService: ActorRef[ChatService.Command]): Behavior[Command] = {
import ChatMessageType._
Behaviors
.receiveMessagePartial[Command] {
case SetSession(newSession) =>
active(newSession, chatService)
case JoinChannel(channel) =>
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
channels ++= List(channel)
Behaviors.same
case LeaveChannel(channel) =>
chatService ! ChatService.LeaveChannel(chatServiceAdapter, channel)
channels = channels.filter(_ == channel)
Behaviors.same
case Message(message) =>
log.info("Chat: " + message)
(message.messageType, message.recipient.trim, message.contents.trim) match {
case (CMT_FLY, recipient, contents) if session.account.gm =>
val flying = contents match {
case "on" => true
case "off" => false
case _ => !session.flying
}
sessionActor ! SessionActor.SetFlying(flying)
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_FLY, false, recipient, if (flying) "on" else "off", None)
)
case (CMT_ANONYMOUS, _, _) =>
// ?
case (CMT_TOGGLE_GM, _, _) =>
// ?
case (CMT_CULLWATERMARK, _, contents) =>
val connectionState =
if (contents.contains("40 80")) 100
else if (contents.contains("120 200")) 25
else 50
sessionActor ! SessionActor.SetConnectionState(connectionState)
case (CMT_SPEED, recipient, contents) if session.account.gm =>
val speed =
try {
contents.toFloat
} catch {
case _: Throwable =>
1f
}
sessionActor ! SessionActor.SetSpeed(speed)
sessionActor ! SessionActor.SendResponse(message.copy(contents = f"$speed%.3f"))
case (CMT_TOGGLESPECTATORMODE, _, contents) if session.account.gm =>
val spectator = contents match {
case "on" => true
case "off" => false
case _ => !session.player.spectator
}
sessionActor ! SessionActor.SetSpectator(spectator)
sessionActor ! SessionActor.SendResponse(message.copy(contents = if (spectator) "on" else "off"))
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_227,
contents = if (spectator) "@SpectatorEnabled" else "@SpectatorDisabled"
)
)
case (CMT_RECALL, _, _) =>
val errorMessage = session.zoningType match {
case Zoning.Method.Quit => Some("You can't recall to your sanctuary continent while quitting")
case Zoning.Method.InstantAction =>
Some("You can't recall to your sanctuary continent while instant actioning")
case Zoning.Method.Recall => Some("You already requested to recall to your sanctuary continent")
case _ if session.zone.id == Zones.sanctuaryZoneId(session.player.Faction) =>
Some("You can't recall to your sanctuary when you are already in your sanctuary")
case _ if !session.player.isAlive || session.deadState != DeadState.Alive =>
Some(if (session.player.isAlive) "@norecall_deconstructing" else "@norecall_dead")
case _ if session.player.VehicleSeated.nonEmpty => Some("@norecall_invehicle")
case _ => None
}
errorMessage match {
case Some(errorMessage) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_QUIT,
false,
"",
errorMessage,
None
)
)
case None =>
sessionActor ! SessionActor.Recall()
}
case (CMT_INSTANTACTION, _, _) =>
if (session.zoningType == Zoning.Method.Quit) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None)
)
} else if (session.zoningType == Zoning.Method.InstantAction) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)
)
} else if (session.zoningType == Zoning.Method.Recall) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_QUIT,
false,
"",
"You won't instant action. You already requested to recall to your sanctuary continent",
None
)
)
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None)
)
} else {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None)
)
}
} else if (session.player.VehicleSeated.nonEmpty) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None)
)
} else {
sessionActor ! SessionActor.InstantAction()
}
case (CMT_QUIT, _, _) =>
if (session.zoningType == Zoning.Method.Quit) {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_quitting", None))
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None)
)
} else {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_dead", None))
}
} else if (session.player.VehicleSeated.nonEmpty) {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_invehicle", None))
} else {
sessionActor ! SessionActor.Quit()
}
case (CMT_SUICIDE, _, _) =>
if (session.player.isAlive && session.deadState != DeadState.Release) {
sessionActor ! SessionActor.Suicide()
}
case (CMT_DESTROY, _, contents) =>
val guid = contents.toInt
session.zone.GUID(session.zone.map.terminalToSpawnPad.getOrElse(guid, guid)) match {
case Some(pad: VehicleSpawnPad) =>
pad.Actor ! VehicleSpawnControl.ProcessControl.Flush
case Some(turret: FacilityTurret) if turret.isUpgrading =>
WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None)
case _ =>
sessionActor ! SessionActor.SendResponse(
PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))).packet
)
}
sessionActor ! SessionActor.SendResponse(message)
/** Messages starting with ! are custom chat commands */
case (messageType, recipient, contents) if contents.startsWith("!") =>
(messageType, recipient, contents) match {
case (_, _, contents) if contents.startsWith("!whitetext ") && session.account.gm =>
chatService ! ChatService.Message(
session,
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
ChatChannel.Default()
)
case (_, _, "!loc") =>
val continent = session.zone
val player = session.player
val loc =
s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
log.info(loc)
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
case (_, _, contents) if contents.startsWith("!list") =>
val zone = contents.split(" ").lift(1) match {
case None =>
Some(session.zone)
case Some(id) =>
Zones.zones.find(_.id == id)
}
zone match {
case Some(zone) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
"\\#8Name (Faction) [ID] at PosX PosY PosZ",
message.note
)
)
(zone.LivePlayers ++ zone.Corpses)
.filter(_.CharId != session.player.CharId)
.sortBy(_.Name)
.foreach(player => {
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
s"\\#7${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
message.note
)
)
})
case None =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
"Invalid zone ID",
message.note
)
)
}
case (_, _, contents) if session.account.gm && contents.startsWith("!kick") =>
val input = contents.split("\\s+").drop(1)
if (input.length > 0) {
val numRegex = raw"(\d+)".r
val id = input(0)
val determination: Player => Boolean = id match {
case numRegex(_) => _.CharId == id.toLong
case _ => _.Name.equals(id)
}
session.zone.LivePlayers
.find(determination)
.orElse(session.zone.Corpses.find(determination)) match {
case Some(player) =>
input.lift(1) match {
case Some(numRegex(time)) =>
sessionActor ! SessionActor.Kick(player, Some(time.toLong))
case _ =>
sessionActor ! SessionActor.Kick(player)
}
case None =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
"Invalid player",
message.note
)
)
}
}
case (_, _, contents) if contents.startsWith("!ntu") && session.account.gm =>
session.zone.Buildings.values.foreach(building =>
building.Amenities.foreach(amenity =>
amenity.Definition match {
case GlobalDefinitions.resource_silo =>
val r = new scala.util.Random
val silo = amenity.asInstanceOf[ResourceSilo]
val ntu: Int = 900 + r.nextInt(100) - silo.NtuCapacitor
silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu)
case _ => ;
}
)
)
case _ =>
// unknown ! commands are ignored
}
case (CMT_CAPTUREBASE, _, contents) if session.account.gm =>
val args = contents.split(" ").filter(_ != "")
val (faction, factionPos) = args.zipWithIndex
.map { case (faction, pos) => (faction.toLowerCase, pos) }
.flatMap {
case ("tr", pos) => Some(PlanetSideEmpire.TR, pos)
case ("nc", pos) => Some(PlanetSideEmpire.NC, pos)
case ("vs", pos) => Some(PlanetSideEmpire.VS, pos)
case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos)
case _ => None
}
.headOption match {
case Some((faction, pos)) => (faction, Some(pos))
case None => (session.player.Faction, None)
}
val (buildingsOption, buildingPos) = args.zipWithIndex.flatMap {
case (_, pos) if factionPos.isDefined && factionPos.get == pos => None
case ("all", pos) =>
Some(
Some(
session.zone.Buildings
.filter {
case (_, building) => building.CaptureTerminal.isDefined
}
.values
.toSeq
),
Some(pos)
)
case (name, pos) =>
session.zone.Buildings.find {
case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
} match {
case Some((_, building)) => Some(Some(Seq(building)), Some(pos))
case None =>
try {
// check if we have a timer
name.toInt
None
} catch {
case _: Throwable =>
Some(None, Some(pos))
}
}
}.headOption match {
case Some((buildings, pos)) => (buildings, pos)
case None => (None, None)
}
val (timerOption, timerPos) = args.zipWithIndex.flatMap {
case (_, pos)
if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos =>
None
case (timer, pos) =>
try {
val t = timer.toInt // TODO what is the timer format supposed to be?
Some(Some(t), Some(pos))
} catch {
case _: Throwable =>
Some(None, Some(pos))
}
}.headOption match {
case Some((timer, posOption)) => (timer, posOption)
case None => (None, None)
}
(factionPos, buildingPos, timerPos, buildingsOption, timerOption) match {
case // [[<empire>|none [<timer>]]
(Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) |
(None, None, None, None, None) |
// [<building name> [<empire>|none [timer]]]
(None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) |
// [all [<empire>|none]]
(Some(1) | None, Some(0), None, Some(_), None) =>
val buildings = buildingsOption.getOrElse(
session.zone.Buildings
.filter {
case (_, building) =>
building.PlayersInSOI.exists { soiPlayer =>
session.player.CharId == soiPlayer.CharId
}
}
.map { case (_, building) => building }
)
buildings foreach { building =>
// TODO implement timer
building.Actor ! BuildingActor.SetFaction(faction)
}
case (_, Some(0), _, None, _) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
true,
"",
s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.",
None
)
)
case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
true,
"",
s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.",
None
)
)
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@@CMT_CAPTUREBASE_usage")
)
}
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
if session.account.gm =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_GMTELL, _, _) if session.account.gm =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_GMBROADCASTPOPUP, _, _) if session.account.gm =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (_, "tr", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0)
)
case (_, "nc", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0)
)
case (_, "vs", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0)
)
case (_, "bo", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)
)
case (CMT_OPEN, _, _) if !session.player.silenced =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_VOICE, _, _) =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_TELL, _, _) if !session.player.silenced =>
chatService ! ChatService.Message(
session,
message,
ChatChannel.Default()
)
case (CMT_BROADCAST, _, _) if !session.player.silenced =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_PLATOON, _, _) if !session.player.silenced =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_COMMAND, _, _) if session.account.gm =>
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
ChatChannel.Default()
)
case (CMT_NOTE, _, _) =>
chatService ! ChatService.Message(session, message, ChatChannel.Default())
case (CMT_SILENCE, _, _) if session.account.gm =>
chatService ! ChatService.Message(session, message, ChatChannel.Default())
case (CMT_SQUAD, _, _) =>
channels.foreach {
case channel: ChatChannel.Squad =>
chatService ! ChatService.Message(session, message.copy(recipient = session.player.Name), channel)
case _ =>
}
case (
CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS,
_,
_
) =>
val players = session.zone.Players
val popTR = players.count(_.faction == PlanetSideEmpire.TR)
val popNC = players.count(_.faction == PlanetSideEmpire.NC)
val popVS = players.count(_.faction == PlanetSideEmpire.VS)
val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)
)
case (CMT_ZONE, _, contents) if session.account.gm =>
val buffer = contents.toLowerCase.split("\\s+")
val (zone, gate, list) = (buffer.lift(0), buffer.lift(1)) match {
case (Some("-list"), None) =>
(None, None, true)
case (Some(zoneId), Some("-list")) =>
(PointOfInterest.get(zoneId), None, true)
case (Some(zoneId), gateId) =>
val zone = PointOfInterest.get(zoneId)
val gate = (zone, gateId) match {
case (Some(zone), Some(gateId)) => PointOfInterest.getWarpgate(zone, gateId)
case (Some(zone), None) => Some(PointOfInterest.selectRandom(zone))
case _ => None
}
(zone, gate, false)
case _ =>
(None, None, false)
}
(zone, gate, list) match {
case (None, None, true) =>
sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, true, "", PointOfInterest.list, None))
case (Some(zone), None, true) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", PointOfInterest.listWarpgates(zone), None)
)
case (Some(zone), Some(gate), false) =>
sessionActor ! SessionActor.SetZone(zone.zonename, gate)
case (_, None, false) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", "Gate id not defined (use '/zone <zone> -list')", None)
)
case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_ZONE_usage")
)
}
case (CMT_WARP, _, contents) if session.account.gm =>
val buffer = contents.toLowerCase.split("\\s+")
val (coordinates, waypoint) = (buffer.lift(0), buffer.lift(1), buffer.lift(2)) match {
case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None)
case (Some("to"), Some(character), None) => (None, None) // TODO not implemented
case (Some("near"), Some(objectName), None) => (None, None) // TODO not implemented
case (Some(waypoint), None, None) => (None, Some(waypoint))
case _ => (None, None)
}
(coordinates, waypoint) match {
case (Some((x, y, z)), None) if List(x, y, z).forall { str =>
val coordinate = str.toFloatOption
coordinate.isDefined && coordinate.get >= 0 && coordinate.get <= 8191
} =>
sessionActor ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat))
case (None, Some(waypoint)) if waypoint != "-help" =>
PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
case Some(location) => sessionActor ! SessionActor.SetPosition(location)
case None =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", s"unknown location '$waypoint'", None)
)
}
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_WARP_usage")
)
}
case (CMT_SETBATTLERANK, _, contents) if session.account.gm =>
val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(target), Some(rank)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetBep(rank.experience)
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetBattleRank"))
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
)
}
case (CMT_SETCOMMANDRANK, _, contents) if session.account.gm =>
val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(target), Some(rank)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetCep(rank.experience)
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
)
}
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if session.account.gm =>
contents.toIntOption match {
case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep)
case None =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_ADDBATTLEEXPERIENCE_usage")
)
}
case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if session.account.gm =>
contents.toIntOption match {
case Some(cep) => avatarActor ! AvatarActor.AwardCep(cep)
case None =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_ADDCOMMANDEXPERIENCE_usage")
)
}
case (CMT_TOGGLE_HAT, _, contents) =>
val cosmetics = session.avatar.cosmetics.getOrElse(Set())
val nextCosmetics = contents match {
case "off" =>
cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
case _ =>
if (cosmetics.contains(Cosmetic.BrimmedCap)) {
cosmetics.diff(Set(Cosmetic.BrimmedCap)) + Cosmetic.Beret
} else if (cosmetics.contains(Cosmetic.Beret)) {
cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
} else {
cosmetics + Cosmetic.BrimmedCap
}
}
val on = nextCosmetics.contains(Cosmetic.BrimmedCap) || nextCosmetics.contains(Cosmetic.Beret)
avatarActor ! AvatarActor.SetCosmetics(nextCosmetics)
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_229,
contents = s"@CMT_TOGGLE_HAT_${if (on) "on" else "off"}"
)
)
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
val cosmetics = session.avatar.cosmetics.getOrElse(Set())
val cosmetic = message.messageType match {
case CMT_HIDE_HELMET => Cosmetic.NoHelmet
case CMT_TOGGLE_SHADES => Cosmetic.Sunglasses
case CMT_TOGGLE_EARPIECE => Cosmetic.Earpiece
}
val on = contents match {
case "on" => true
case "off" => false
case _ => !cosmetics.contains(cosmetic)
}
avatarActor ! AvatarActor.SetCosmetics(
if (on) cosmetics + cosmetic
else cosmetics.diff(Set(cosmetic))
)
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_229,
contents = s"@${message.messageType.toString}_${if (on) "on" else "off"}"
)
)
case (CMT_ADDCERTIFICATION, _, contents) if session.account.gm =>
val certs = contents.split(" ").filter(_ != "").map(name => Certification.values.find(_.name == name))
if (certs.nonEmpty) {
if (certs.contains(None)) {
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_229,
contents = s"@AckErrorCertifications"
)
)
} else {
avatarActor ! AvatarActor.SetCertifications(session.avatar.certifications ++ certs.flatten)
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_229,
contents = s"@AckSuccessCertifications"
)
)
}
} else {
if (session.avatar.certifications.size < Certification.values.size) {
avatarActor ! AvatarActor.SetCertifications(Certification.values.toSet)
} else {
avatarActor ! AvatarActor.SetCertifications(Set())
}
sessionActor ! SessionActor.SendResponse(
message.copy(
messageType = UNK_229,
contents = s"@AckSuccessCertifications"
)
)
}
case _ =>
log.info(s"unhandled chat message $message")
}
Behaviors.same
case IncomingMessage(fromSession, message, channel) =>
message.messageType match {
case CMT_TELL | U_CMT_TELLFROM | CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | UNK_45 | UNK_71 |
CMT_NOTE | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_227 =>
sessionActor ! SessionActor.SendResponse(message)
case CMT_OPEN =>
if (
session.zone == fromSession.zone &&
Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 &&
session.player.Faction == fromSession.player.Faction
) {
sessionActor ! SessionActor.SendResponse(message)
}
case CMT_VOICE =>
if (
session.zone == fromSession.zone &&
Vector3.Distance(session.player.Position, fromSession.player.Position) < 25
) {
sessionActor ! SessionActor.SendResponse(message)
}
case CMT_SILENCE =>
val args = message.contents.split(" ")
val (name, time) = (args.lift(0), args.lift(1)) match {
case (Some(name), _) if name != session.player.Name =>
log.error("received silence message for other player")
(None, None)
case (Some(name), None) => (Some(name), Some(5))
case (Some(name), Some(time)) if time.toIntOption.isDefined => (Some(name), Some(time.toInt))
case _ => (None, None)
}
(name, time) match {
case (Some(_), Some(time)) =>
if (session.player.silenced) {
sessionActor ! SessionActor.SetSilenced(false)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_off", None)
)
if (!silenceTimer.isCancelled) silenceTimer.cancel()
} else {
sessionActor ! SessionActor.SetSilenced(true)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_on", None)
)
silenceTimer = context.system.scheduler.scheduleOnce(
time minutes,
() => {
sessionActor ! SessionActor.SetSilenced(false)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_timeout", None)
)
}
)
}
case (name, time) =>
log.error(s"bad silence args $name $time")
}
case _ =>
log.error(s"unexpected messageType $message")
}
Behaviors.same
}
.receiveSignal {
case (_, _: PostStop) =>
silenceTimer.cancel()
chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)
Behaviors.same
case _ =>
Behaviors.same
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,175 @@
package net.psforever.actors.zone
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.{actor => classic}
import net.psforever.actors.commands.NtuCommand
import net.psforever.objects.serverobject.structures.{Building, WarpGate}
import net.psforever.objects.zones.Zone
import net.psforever.persistence
import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Database._
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.{InterstellarClusterService, ServiceManager}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
object BuildingActor {
def apply(zone: Zone, building: Building): Behavior[Command] =
Behaviors
.supervise[Command] {
Behaviors.withStash(100) { buffer =>
Behaviors.setup(context => new BuildingActor(context, buffer, zone, building).start())
}
}
.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
private case class ReceptionistListing(listing: Receptionist.Listing) extends Command
private case class ServiceManagerLookupResult(result: ServiceManager.LookupResult) extends Command
final case class SetFaction(faction: PlanetSideEmpire.Value) extends Command
// TODO remove
// Changes to building objects should go through BuildingActor
// Once they do, we won't need this anymore
final case class MapUpdate() extends Command
final case class Ntu(command: NtuCommand.Command) extends Command
}
class BuildingActor(
context: ActorContext[BuildingActor.Command],
buffer: StashBuffer[BuildingActor.Command],
zone: Zone,
building: Building
) {
import BuildingActor._
private[this] val log = org.log4s.getLogger
var galaxyService: Option[classic.ActorRef] = None
var interstellarCluster: Option[ActorRef[InterstellarClusterService.Command]] = None
context.system.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.messageAdapter[Receptionist.Listing](ReceptionistListing)
)
ServiceManager.serviceManager ! ServiceManager.LookupFromTyped(
"galaxy",
context.messageAdapter[ServiceManager.LookupResult](ServiceManagerLookupResult)
)
def start(): Behavior[Command] = {
Behaviors.receiveMessage {
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
interstellarCluster = listings.headOption
postStartBehaviour()
case ServiceManagerLookupResult(ServiceManager.LookupResult(request, endpoint)) =>
request match {
case "galaxy" => galaxyService = Some(endpoint)
}
postStartBehaviour()
case other =>
buffer.stash(other)
Behaviors.same
}
}
def postStartBehaviour(): Behavior[Command] = {
(galaxyService, interstellarCluster) match {
case (Some(galaxyService), Some(interstellarCluster)) =>
buffer.unstashAll(active(galaxyService, interstellarCluster))
case _ =>
Behaviors.same
}
}
def active(
galaxyService: classic.ActorRef,
interstellarCluster: ActorRef[InterstellarClusterService.Command]
): Behavior[Command] = {
Behaviors.receiveMessagePartial {
case SetFaction(faction) =>
import ctx._
ctx
.run(
query[persistence.Building]
.filter(_.localId == lift(building.MapId))
.filter(_.zoneId == lift(zone.Number))
)
.onComplete {
case Success(res) =>
res.headOption match {
case Some(_) =>
ctx
.run(
query[persistence.Building]
.filter(_.localId == lift(building.MapId))
.filter(_.zoneId == lift(zone.Number))
.update(_.factionId -> lift(building.Faction.id))
)
.onComplete {
case Success(_) =>
case Failure(e) => log.error(e.getMessage)
}
case _ =>
ctx
.run(
query[persistence.Building]
.insert(
_.localId -> lift(building.MapId),
_.factionId -> lift(building.Faction.id),
_.zoneId -> lift(zone.Number)
)
)
.onComplete {
case Success(_) =>
case Failure(e) => log.error(e.getMessage)
}
}
case Failure(e) => log.error(e.getMessage)
}
building.Faction = faction
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
Behaviors.same
case MapUpdate() =>
galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(building.infoUpdateMessage()))
Behaviors.same
case Ntu(msg) =>
ntu(msg)
}
}
def ntu(msg: NtuCommand.Command): Behavior[Command] = {
import NtuCommand._
val ntuBuilding = building match {
case b: WarpGate => b
case _ => return Behaviors.unhandled
}
msg match {
case Offer(source) =>
case Request(amount, replyTo) =>
ntuBuilding match {
case warpGate: WarpGate => replyTo ! Grant(warpGate, if (warpGate.Active) amount else 0)
case _ => return Behaviors.unhandled
}
}
Behaviors.same
}
}

View file

@ -0,0 +1,130 @@
package net.psforever.actors.zone
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.StructureType
import net.psforever.objects.zones.Zone
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.collection.mutable.ListBuffer
import akka.actor.typed.scaladsl.adapter._
import net.psforever.util.Database._
import net.psforever.persistence
import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global
object ZoneActor {
def apply(zone: Zone): Behavior[Command] =
Behaviors
.supervise[Command] {
Behaviors.setup(context => new ZoneActor(context, zone))
}
.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
final case class GetZone(replyTo: ActorRef[ZoneResponse]) extends Command
final case class ZoneResponse(zone: Zone)
/*
final case class AddAvatar(avatar: Avatar) extends Command
final case class RemoveAvatar(avatar: Avatar) extends Command
*/
final case class AddPlayer(player: Player) extends Command
final case class RemovePlayer(player: Player) extends Command
final case class DropItem(item: Equipment, position: Vector3, orientation: Vector3) extends Command
final case class PickupItem(guid: PlanetSideGUID) extends Command
final case class BuildDeployable(obj: PlanetSideGameObject with Deployable, withTool: ConstructionItem)
extends Command
final case class DismissDeployable(obj: PlanetSideGameObject with Deployable) extends Command
final case class SpawnVehicle(vehicle: Vehicle) extends Command
final case class DespawnVehicle(vehicle: Vehicle) extends Command
final case class HotSpotActivity(defender: SourceEntry, attacker: SourceEntry, location: Vector3) extends Command
// TODO remove
// Changes to zone objects should go through ZoneActor
// Once they do, we won't need this anymore
final case class ZoneMapUpdate() extends Command
}
class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
extends AbstractBehavior[ZoneActor.Command](context) {
import ZoneActor._
import ctx._
private[this] val log = org.log4s.getLogger
val players: ListBuffer[Player] = ListBuffer()
zone.actor = context.self
zone.init(context.toClassic)
ctx.run(query[persistence.Building].filter(_.zoneId == lift(zone.Number))).onComplete {
case Success(buildings) =>
buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match {
case Some(b) => b.Faction = PlanetSideEmpire(building.factionId)
case None => // TODO this happens during testing, need a way to not always persist during tests
}
}
case Failure(e) => log.error(e.getMessage)
}
override def onMessage(msg: Command): Behavior[Command] = {
msg match {
case GetZone(replyTo) =>
replyTo ! ZoneResponse(zone)
case AddPlayer(player) =>
players.addOne(player)
case RemovePlayer(player) =>
players.filterInPlace(p => p.CharId == player.CharId)
case DropItem(item, position, orientation) =>
zone.Ground ! Zone.Ground.DropItem(item, position, orientation)
case PickupItem(guid) =>
zone.Ground ! Zone.Ground.PickupItem(guid)
case BuildDeployable(obj, tool) =>
zone.Deployables ! Zone.Deployable.Build(obj, tool)
case DismissDeployable(obj) =>
zone.Deployables ! Zone.Deployable.Dismiss(obj)
case SpawnVehicle(vehicle) =>
zone.Transport ! Zone.Vehicle.Spawn(vehicle)
case DespawnVehicle(vehicle) =>
zone.Transport ! Zone.Vehicle.Despawn(vehicle)
case HotSpotActivity(defender, attacker, location) =>
zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location)
case ZoneMapUpdate() =>
zone.Buildings
.filter(_._2.BuildingType == StructureType.Facility)
.values
.foreach(_.Actor ! BuildingActor.MapUpdate())
}
this
}
}

View file

@ -0,0 +1,310 @@
// Copyright (c) 2017 PSForever
package net.psforever.crypto
import com.sun.jna.ptr.IntByReference
import net.psforever.IFinalizable
import sna.Library
import com.sun.jna.Pointer
import scodec.bits.ByteVector
object CryptoInterface {
final val libName = "pscrypto"
final val fullLibName = libName
final val PSCRYPTO_VERSION_MAJOR = 1
final val PSCRYPTO_VERSION_MINOR = 1
/**
* NOTE: this is a single, global shared library for the entire server's crypto needs
*
* Unfortunately, access to this object didn't used to be synchronized. I noticed that
* tests for this module were hanging ("arrive at a shared secret" & "must fail to agree on
* a secret..."). This heisenbug was responsible for failed Travis test runs and developer
* issues as well. Using Windows minidumps, I tracked the issue to a single thread deep in
* pscrypto.dll. It appeared to be executing an EB FE instruction (on Intel x86 this is
* `jmp $-2` or jump to self), which is an infinite loop. The stack trace made little to no
* sense and after banging my head on the wall for many hours, I assumed that something deep
* in CryptoPP, the libgcc libraries, or MSVC++ was the cause (or myself). Now all access to
* pscrypto functions that allocate and deallocate memory (DH_Start, RC5_Init) are synchronized.
* This *appears* to have fixed the problem.
*/
final val psLib = new Library(libName)
final val RC5_BLOCK_SIZE = 8
final val MD5_MAC_SIZE = 16
val functionsList = List(
"PSCrypto_Init",
"PSCrypto_Get_Version",
"PSCrypto_Version_String",
"RC5_Init",
"RC5_Encrypt",
"RC5_Decrypt",
"DH_Start",
"DH_Start_Generate",
"DH_Agree",
"MD5_MAC",
"Free_DH",
"Free_RC5"
)
/**
* Used to initialize the crypto library at runtime. The version is checked and
* all functions are mapped.
*/
def initialize(): Unit = {
// preload all library functions for speed
functionsList foreach psLib.prefetch
val libraryMajor = new IntByReference
val libraryMinor = new IntByReference
psLib.PSCrypto_Get_Version(libraryMajor, libraryMinor)[Unit]
if (!psLib.PSCrypto_Init(PSCRYPTO_VERSION_MAJOR, PSCRYPTO_VERSION_MINOR)[Boolean]) {
throw new IllegalArgumentException(
s"Invalid PSCrypto library version ${libraryMajor.getValue}.${libraryMinor.getValue}. Expected " +
s"$PSCRYPTO_VERSION_MAJOR.$PSCRYPTO_VERSION_MINOR"
)
}
}
/**
* Used for debugging object loading
*/
def printEnvironment(): Unit = {
import java.io.File
val classpath = System.getProperty("java.class.path")
val classpathEntries = classpath.split(File.pathSeparator)
val myLibraryPath = System.getProperty("user.dir")
val jnaLibrary = System.getProperty("jna.library.path")
val javaLibrary = System.getProperty("java.library.path")
println("User dir: " + myLibraryPath)
println("JNA Lib: " + jnaLibrary)
println("Java Lib: " + javaLibrary)
print("Classpath: ")
classpathEntries.foreach(println)
println("Required data model: " + System.getProperty("sun.arch.data.model"))
}
def MD5MAC(key: ByteVector, message: ByteVector, bytesWanted: Int): ByteVector = {
val out = Array.ofDim[Byte](bytesWanted)
// WARNING BUG: the function must be cast to something (even if void) otherwise it doesnt work
val ret = psLib.MD5_MAC(key.toArray, key.length, message.toArray, message.length, out, out.length)[Boolean]
if (!ret)
throw new Exception("MD5MAC failed to process")
ByteVector(out)
}
/**
* Checks if two Message Authentication Codes are the same in constant time,
* preventing a timing attack for MAC forgery
*
* @param mac1 A MAC value
* @param mac2 Another MAC value
*/
def verifyMAC(mac1: ByteVector, mac2: ByteVector): Boolean = {
var okay = true
// prevent byte by byte guessing
if (mac1.length != mac2.length)
return false
for (i <- 0 until mac1.length.toInt) {
okay = okay && mac1 { i } == mac2 { i }
}
okay
}
class CryptoDHState extends IFinalizable {
var started = false
// these types MUST be Arrays of bytes for JNA to work
val privateKey = Array.ofDim[Byte](16)
val publicKey = Array.ofDim[Byte](16)
val p = Array.ofDim[Byte](16)
val g = Array.ofDim[Byte](16)
var dhHandle = Pointer.NULL
def start(modulus: ByteVector, generator: ByteVector): Unit = {
assertNotClosed
if (started)
throw new IllegalStateException("DH state has already been started")
psLib.synchronized {
dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer]
}
if (dhHandle == Pointer.NULL)
throw new Exception("DH initialization failed!")
modulus.copyToArray(p, 0)
generator.copyToArray(g, 0)
started = true
}
def start(): Unit = {
assertNotClosed
if (started)
throw new IllegalStateException("DH state has already been started")
psLib.synchronized {
dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer]
}
if (dhHandle == Pointer.NULL)
throw new Exception("DH initialization failed!")
started = true
}
def agree(otherPublicKey: ByteVector) = {
if (!started)
throw new IllegalStateException("DH state has not been started")
val agreedValue = Array.ofDim[Byte](16)
val agreed = psLib.DH_Agree(dhHandle, agreedValue, privateKey, otherPublicKey.toArray)[Boolean]
if (!agreed)
throw new Exception("Failed to DH agree")
ByteVector.view(agreedValue)
}
private def checkAndReturnView(array: Array[Byte]) = {
if (!started)
throw new IllegalStateException("DH state has not been started")
ByteVector.view(array)
}
def getPrivateKey = {
checkAndReturnView(privateKey)
}
def getPublicKey = {
checkAndReturnView(publicKey)
}
def getModulus = {
checkAndReturnView(p)
}
def getGenerator = {
checkAndReturnView(g)
}
override def close = {
if (started) {
// TODO: zero private key material
psLib.synchronized {
psLib.Free_DH(dhHandle)[Unit]
}
started = false
}
super.close
}
}
class CryptoState(val decryptionKey: ByteVector, val encryptionKey: ByteVector) extends IFinalizable {
// Note that the keys must be returned as primitive Arrays for JNA to work
var encCryptoHandle: Pointer = Pointer.NULL
var decCryptoHandle: Pointer = Pointer.NULL
psLib.synchronized {
encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer]
decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer]
}
if (encCryptoHandle == Pointer.NULL)
throw new Exception("Encryption initialization failed!")
if (decCryptoHandle == Pointer.NULL)
throw new Exception("Decryption initialization failed!")
def encrypt(plaintext: ByteVector): ByteVector = {
if (plaintext.length % RC5_BLOCK_SIZE != 0)
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
val ciphertext = Array.ofDim[Byte](plaintext.length.toInt)
val ret = psLib.RC5_Encrypt(encCryptoHandle, plaintext.toArray, plaintext.length, ciphertext)[Boolean]
if (!ret)
throw new Exception("Failed to encrypt plaintext")
ByteVector.view(ciphertext)
}
def decrypt(ciphertext: ByteVector): ByteVector = {
if (ciphertext.length % RC5_BLOCK_SIZE != 0)
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
val plaintext = Array.ofDim[Byte](ciphertext.length.toInt)
val ret = psLib.RC5_Decrypt(decCryptoHandle, ciphertext.toArray, ciphertext.length, plaintext)[Boolean]
if (!ret)
throw new Exception("Failed to decrypt ciphertext")
ByteVector.view(plaintext)
}
override def close = {
psLib.synchronized {
psLib.Free_RC5(encCryptoHandle)[Unit]
psLib.Free_RC5(decCryptoHandle)[Unit]
}
super.close
}
}
class CryptoStateWithMAC(
decryptionKey: ByteVector,
encryptionKey: ByteVector,
val decryptionMACKey: ByteVector,
val encryptionMACKey: ByteVector
) extends CryptoState(decryptionKey, encryptionKey) {
/**
* Performs a MAC operation over the message. Used when encrypting packets
*
* @param message the input message
* @return ByteVector
*/
def macForEncrypt(message: ByteVector): ByteVector = {
MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
}
/**
* Performs a MAC operation over the message. Used when verifying decrypted packets
*
* @param message the input message
* @return ByteVector
*/
def macForDecrypt(message: ByteVector): ByteVector = {
MD5MAC(decryptionMACKey, message, MD5_MAC_SIZE)
}
/**
* MACs the plaintext message, encrypts it, and then returns the encrypted message with the
* MAC appended to the end.
*
* @param message Arbitrary set of bytes
* @return ByteVector
*/
def macAndEncrypt(message: ByteVector): ByteVector = {
encrypt(message) ++ MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
}
}
}

View file

@ -0,0 +1,358 @@
package net.psforever.login
import java.security.SecureRandom
import akka.actor.MDCContextAware.Implicits._
import akka.actor.{Actor, ActorRef, MDCContextAware}
import net.psforever.crypto.CryptoInterface
import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC
import net.psforever.packet._
import net.psforever.packet.control._
import net.psforever.packet.crypto._
import net.psforever.packet.game.PingMsg
import org.log4s.MDC
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
sealed trait CryptoSessionAPI
final case class DropCryptoSession() extends CryptoSessionAPI
/**
* Actor that stores crypto state for a connection, appropriately encrypts and decrypts packets,
* and passes packets along to the next hop once processed.
*/
class CryptoSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger
var sessionId: Long = 0
var leftRef: ActorRef = ActorRef.noSender
var rightRef: ActorRef = ActorRef.noSender
var cryptoDHState: Option[CryptoInterface.CryptoDHState] = None
var cryptoState: Option[CryptoInterface.CryptoStateWithMAC] = None
val random = new SecureRandom()
// crypto handshake state
var serverChallenge = ByteVector.empty
var serverChallengeResult = ByteVector.empty
var serverMACBuffer = ByteVector.empty
var clientPublicKey = ByteVector.empty
var clientChallenge = ByteVector.empty
var clientChallengeResult = ByteVector.empty
var clientNonce: Long = 0
var serverNonce: Long = 0
// Don't leak crypto object memory even on an exception
override def postStop() = {
cleanupCrypto()
}
def receive = Initializing
def Initializing: Receive = {
case HelloFriend(sharedSessionId, pipe) =>
import MDCContextAware.Implicits._
this.sessionId = sharedSessionId
leftRef = sender()
if (pipe.hasNext) {
rightRef = pipe.next() // who ever we send to has to send something back to us
rightRef !> HelloFriend(sessionId, pipe)
} else {
rightRef = sender()
}
log.trace(s"Left sender ${leftRef.path.name}")
context.become(NewClient)
case default =>
log.error("Unknown message " + default)
context.stop(self)
}
def NewClient: Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg) match {
case Successful(p) =>
log.trace("Initializing -> NewClient")
p match {
case ControlPacket(_, ClientStart(nonce)) =>
clientNonce = nonce
serverNonce = Math.abs(random.nextInt())
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, serverNonce)))
log.trace(s"ClientStart($nonce), $serverNonce")
context.become(CryptoExchange)
case _ =>
log.error(s"Unexpected packet type $p in state NewClient")
}
case Failure(_) =>
// There is a special case where no crypto is being used.
// The only packet coming through looks like PingMsg. This is a hardcoded
// feature of the client @ 0x005FD618
PacketCoding.DecodePacket(msg) match {
case Successful(packet) =>
packet match {
case ping @ PingMsg(_, _) =>
// reflect the packet back to the sender
sendResponse(ping)
case _ =>
log.error(s"Unexpected non-crypto packet type $packet in state NewClient")
}
case Failure(e) =>
log.error("Could not decode packet: " + e + s" in state NewClient")
}
}
case default =>
log.error(s"Invalid message '$default' received in state NewClient")
}
def CryptoExchange: Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
case Failure(e) =>
log.error("Could not decode packet in state CryptoExchange: " + e)
case Successful(pkt) =>
log.trace("NewClient -> CryptoExchange")
pkt match {
case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) =>
cryptoDHState = Some(new CryptoInterface.CryptoDHState())
val dh = cryptoDHState.get
// initialize our crypto state from the client's P and G
dh.start(p, g)
// save the client challenge
clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge)
// save the packet we got for a MAC check later. drop the first 3 bytes
serverMACBuffer ++= msg.drop(3)
val serverTime = System.currentTimeMillis() / 1000L
val randomChallenge = getRandBytes(0xc)
// store the complete server challenge for later
serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge)
val packet =
PacketCoding.CreateCryptoPacket(seq, ServerChallengeXchg(serverTime, randomChallenge, dh.getPublicKey))
val sentPacket = sendResponse(packet)
// save the sent packet a MAC check
serverMACBuffer ++= sentPacket.drop(3)
context.become(CryptoSetupFinishing)
case _ =>
log.error(s"Unexpected packet type $pkt in state CryptoExchange")
}
}
case default =>
log.error(s"Invalid message '$default' received in state CryptoExchange")
}
def CryptoSetupFinishing: Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
case Failure(e) => log.error("Could not decode packet in state CryptoSetupFinishing: " + e)
case Successful(p) =>
log.trace("CryptoExchange -> CryptoSetupFinishing")
p match {
case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) =>
clientPublicKey = clientPubKey
clientChallengeResult = clientChalResult
// save the packet we got for a MAC check later
serverMACBuffer ++= msg.drop(3)
val dh = cryptoDHState.get
val agreedValue = dh.agree(clientPublicKey)
// we are now done with the DH crypto object
dh.close
/*println("Agreed: " + agreedValue)
println(s"Client challenge: $clientChallenge")*/
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
hex"00000000" ++ serverChallenge ++ hex"00000000"
//println("In message: " + agreedMessage)
val masterSecret = CryptoInterface.MD5MAC(agreedValue, agreedMessage, 20)
//println("Master secret: " + masterSecret)
serverChallengeResult = CryptoInterface.MD5MAC(
masterSecret,
ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01",
0xc
)
// val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret,
// ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01",
// 0xc)
// println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult))
val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
/*println("DecExpansion: " + decExpansion)
println("EncExpansion: " + encExpansion)*/
// expand the encryption and decryption keys
// The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys
val expandedDecKey =
CryptoInterface.MD5MAC(masterSecret, decExpansion, 0x40) // this is what is visible in IDA
val expandedEncKey = CryptoInterface.MD5MAC(masterSecret, encExpansion, 0x40)
val decKey = expandedDecKey.take(20)
val encKey = expandedEncKey.take(20)
val decMACKey = expandedDecKey.drop(20).take(16)
val encMACKey = expandedEncKey.drop(20).take(16)
/*println("**** DecKey: " + decKey)
println("**** EncKey: " + encKey)
println("**** DecMacKey: " + decMACKey)
println("**** EncMacKey: " + encMACKey)*/
// spin up our encryption program
cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey))
val packet = PacketCoding.CreateCryptoPacket(seq, ServerFinished(serverChallengeResult))
sendResponse(packet)
context.become(Established)
case default => failWithError(s"Unexpected packet type $default in state CryptoSetupFinished")
}
}
case default => failWithError(s"Invalid message '$default' received in state CryptoSetupFinished")
}
def Established: Receive = {
//same as having received ad hoc hexadecimal
case RawPacket(msg) =>
if (sender() == rightRef) {
val packet = PacketCoding.encryptPacket(cryptoState.get, 0, msg).require
sendResponse(packet)
} else { //from network-side
PacketCoding.UnmarshalPacket(msg) match {
case Successful(p) =>
p match {
case encPacket @ EncryptedPacket(_ /*seq*/, _) =>
PacketCoding.decryptPacketData(cryptoState.get, encPacket) match {
case Successful(packet) =>
MDC("sessionId") = sessionId.toString
rightRef !> RawPacket(packet)
case Failure(e) =>
log.error("Failed to decode encrypted packet: " + e)
}
case default =>
failWithError(s"Unexpected packet type $default in state Established")
}
case Failure(e) =>
log.error("Could not decode raw packet: " + e)
}
}
//message to self?
case api: CryptoSessionAPI =>
api match {
case DropCryptoSession() =>
handleEstablishedPacket(
sender(),
PacketCoding.CreateControlPacket(TeardownConnection(clientNonce))
)
}
//echo the session router? isn't that normally the leftRef?
case sessionAPI: SessionRouterAPI =>
leftRef !> sessionAPI
//error
case default =>
failWithError(s"Invalid message '$default' received in state Established")
}
def failWithError(error: String) = {
log.error(error)
}
def cleanupCrypto() = {
if (cryptoDHState.isDefined) {
cryptoDHState.get.close
cryptoDHState = None
}
if (cryptoState.isDefined) {
cryptoState.get.close
cryptoState = None
}
}
def resetState(): Unit = {
context.become(receive)
// reset the crypto primitives
cleanupCrypto()
serverChallenge = ByteVector.empty
serverChallengeResult = ByteVector.empty
serverMACBuffer = ByteVector.empty
clientPublicKey = ByteVector.empty
clientChallenge = ByteVector.empty
clientChallengeResult = ByteVector.empty
}
def handleEstablishedPacket(from: ActorRef, cont: PlanetSidePacketContainer): Unit = {
//we are processing a packet that we decrypted
if (from == self) { //to WSA, LSA, etc.
rightRef !> cont
} else if (from == rightRef) { //processing a completed packet from the right; to network-side
PacketCoding.getPacketDataForEncryption(cont) match {
case Successful((seq, data)) =>
val packet = PacketCoding.encryptPacket(cryptoState.get, seq, data).require
sendResponse(packet)
case Failure(ex) =>
log.error(s"$ex")
}
} else {
log.error(s"Invalid sender when handling a message in Established $from")
}
}
def sendResponse(cont: PlanetSidePacketContainer): ByteVector = {
log.trace("CRYPTO SEND: " + cont)
val pkt = PacketCoding.MarshalPacket(cont)
pkt match {
case Failure(_) =>
log.error(s"Failed to marshal packet ${cont.getClass.getName} when sending response")
ByteVector.empty
case Successful(v) =>
val bytes = v.toByteVector
MDC("sessionId") = sessionId.toString
leftRef !> ResponsePacket(bytes)
bytes
}
}
def sendResponse(pkt: PlanetSideGamePacket): ByteVector = {
log.trace("CRYPTO SEND GAME: " + pkt)
val pktEncoded = PacketCoding.EncodePacket(pkt)
pktEncoded match {
case Failure(_) =>
log.error(s"Failed to encode packet ${pkt.getClass.getName} when sending response")
ByteVector.empty
case Successful(v) =>
val bytes = v.toByteVector
MDC("sessionId") = sessionId.toString
leftRef !> ResponsePacket(bytes)
bytes
}
}
def getRandBytes(amount: Int): ByteVector = {
val array = Array.ofDim[Byte](amount)
random.nextBytes(array)
ByteVector.view(array)
}
}

View file

@ -0,0 +1,321 @@
package net.psforever.login
import java.net.{InetAddress, InetSocketAddress}
import akka.actor.MDCContextAware.Implicits._
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import com.github.t3hnar.bcrypt._
import net.psforever.objects.{Account, Default}
import net.psforever.packet.control._
import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus}
import net.psforever.packet.game._
import net.psforever.packet.{PlanetSideGamePacket, _}
import net.psforever.persistence
import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Config
import net.psforever.util.Database._
import org.log4s.MDC
import scodec.bits._
import net.psforever.services.ServiceManager
import net.psforever.services.ServiceManager.Lookup
import net.psforever.services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.{Failure, Success}
class LoginSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger
import scala.concurrent.ExecutionContext.Implicits.global
private case class UpdateServerList()
val usernameRegex = """[A-Za-z0-9]{3,}""".r
var sessionId: Long = 0
var leftRef: ActorRef = ActorRef.noSender
var rightRef: ActorRef = ActorRef.noSender
var accountIntermediary: ActorRef = ActorRef.noSender
var updateServerListTask: Cancellable = Default.Cancellable
var ipAddress: String = ""
var hostName: String = ""
var canonicalHostName: String = ""
var port: Int = 0
val serverName = Config.app.world.serverName
val publicAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
// Reference: https://stackoverflow.com/a/50470009
private val numBcryptPasses = 10
override def postStop() = {
if (updateServerListTask != null)
updateServerListTask.cancel()
}
def receive = Initializing
def Initializing: Receive = {
case HelloFriend(aSessionId, pipe) =>
this.sessionId = aSessionId
leftRef = sender()
if (pipe.hasNext) {
rightRef = pipe.next()
rightRef !> HelloFriend(aSessionId, pipe)
} else {
rightRef = sender()
}
context.become(Started)
ServiceManager.serviceManager ! Lookup("accountIntermediary")
case _ =>
log.error("Unknown message")
context.stop(self)
}
def Started: Receive = {
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
case ReceiveIPAddress(address) =>
ipAddress = address.Address
hostName = address.HostName
canonicalHostName = address.CanonicalHostName
port = address.Port
case UpdateServerList() =>
updateServerList()
case ControlPacket(_, ctrl) =>
handleControlPkt(ctrl)
case GamePacket(_, _, game) =>
handleGamePkt(game)
case default => failWithError(s"Invalid packet class received: $default")
}
def handleControlPkt(pkt: PlanetSideControlPacket) = {
pkt match {
/// TODO: figure out what this is what what it does for the PS client
/// I believe it has something to do with reliable packet transmission and resending
case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) =>
log.trace(s"SYNC: $sync")
val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error
sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick, fa, fb, fb, fa)))
case TeardownConnection(_) =>
sendResponse(DropSession(sessionId, "client requested session termination"))
case default =>
log.error(s"Unhandled ControlPacket $default")
}
}
def handleGamePkt(pkt: PlanetSideGamePacket) =
pkt match {
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) =>
// TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
accountIntermediary ! RetrieveIPAddress(sessionId)
if (token.isDefined)
log.info(s"New login UN:$username Token:${token.get}. $clientVersion")
else {
// log.info(s"New login UN:$username PW:$password. $clientVersion")
log.info(s"New login UN:$username. $clientVersion")
}
accountLogin(username, password.get)
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
log.info(s"Connect to world request for '$name'")
val response = ConnectToWorldMessage(serverName, publicAddress.getAddress.getHostAddress, publicAddress.getPort)
sendResponse(PacketCoding.CreateGamePacket(0, response))
sendResponse(DropSession(sessionId, "user transferring to world"))
case _ =>
log.debug(s"Unhandled GamePacket $pkt")
}
def accountLogin(username: String, password: String): Unit = {
import ctx._
val newToken = this.generateToken()
log.info("accountLogin")
val result = for {
// backwards compatibility: prefer exact match first, then try lowercase
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username)))
accountsLower <- accountsExact.headOption match {
case None =>
ctx.run(query[persistence.Account].filter(_.username.toLowerCase == lift(username).toLowerCase))
case Some(_) =>
Future.successful(Seq())
}
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
case Some(account) => Future.successful(Some(account))
case None => {
Config.app.login.createMissingAccounts match {
case true =>
val passhash: String = password.bcrypt(numBcryptPasses)
ctx.run(
query[persistence.Account]
.insert(_.passhash -> lift(passhash), _.username -> lift(username))
.returningGenerated(_.id)
) flatMap { id => ctx.run(query[persistence.Account].filter(_.id == lift(id))) } map { accounts =>
Some(accounts.head)
}
case false =>
loginFailureResponse(username, newToken)
Future.successful(None)
}
}
}
login <- accountOption match {
case Some(account) =>
log.info(s"$account")
(account.inactive, password.isBcrypted(account.passhash)) match {
case (false, true) =>
accountIntermediary ! StoreAccountData(newToken, Account(account.id, account.username, account.gm))
val future = ctx.run(
query[persistence.Login].insert(
_.accountId -> lift(account.id),
_.ipAddress -> lift(ipAddress),
_.canonicalHostname -> lift(canonicalHostName),
_.hostname -> lift(hostName),
_.port -> lift(port)
)
)
loginSuccessfulResponse(username, newToken)
updateServerListTask =
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 2 seconds, self, UpdateServerList())
future
case (_, false) =>
loginPwdFailureResponse(username, newToken)
Future.successful(None)
case (true, _) =>
loginAccountFailureResponse(username, newToken)
Future.successful(None)
}
case None => Future.successful(None)
}
} yield login
result.onComplete {
case Success(_) =>
case Failure(e) => log.error(e.getMessage())
}
}
def loginSuccessfulResponse(username: String, newToken: String) = {
sendResponse(
PacketCoding.CreateGamePacket(
0,
LoginRespMessage(
newToken,
LoginError.Success,
StationError.AccountActive,
StationSubscriptionStatus.Active,
0,
username,
10001
)
)
)
}
def loginPwdFailureResponse(username: String, newToken: String) = {
log.info(s"Failed login to account $username")
sendResponse(
PacketCoding.CreateGamePacket(
0,
LoginRespMessage(
newToken,
LoginError.BadUsernameOrPassword,
StationError.AccountActive,
StationSubscriptionStatus.Active,
685276011,
username,
10001
)
)
)
}
def loginFailureResponse(username: String, newToken: String) = {
log.info("DB problem")
sendResponse(
PacketCoding.CreateGamePacket(
0,
LoginRespMessage(
newToken,
LoginError.unk1,
StationError.AccountActive,
StationSubscriptionStatus.Active,
685276011,
username,
10001
)
)
)
}
def loginAccountFailureResponse(username: String, newToken: String) = {
log.info(s"Account $username inactive")
sendResponse(
PacketCoding.CreateGamePacket(
0,
LoginRespMessage(
newToken,
LoginError.BadUsernameOrPassword,
StationError.AccountClosed,
StationSubscriptionStatus.Active,
685276011,
username,
10001
)
)
)
}
def generateToken() = {
val r = new scala.util.Random
val sb = new StringBuilder
for (_ <- 1 to 31) {
sb.append(r.nextPrintableChar())
}
sb.toString
}
def updateServerList() = {
val msg = VNLWorldStatusMessage(
"Welcome to PlanetSide! ",
Vector(
WorldInformation(
serverName,
WorldStatus.Up,
Config.app.world.serverType,
Vector(WorldConnectionInfo(publicAddress)),
PlanetSideEmpire.VS
)
)
)
sendResponse(PacketCoding.CreateGamePacket(0, msg))
}
def failWithError(error: String) = {
log.error(error)
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
}
def sendResponse(cont: Any) = {
log.trace("LOGIN SEND: " + cont)
MDC("sessionId") = sessionId.toString
rightRef !> cont
}
def sendRawResponse(pkt: ByteVector) = {
log.trace("LOGIN SEND RAW: " + pkt)
MDC("sessionId") = sessionId.toString
rightRef !> RawPacket(pkt)
}
}

View file

@ -0,0 +1,484 @@
package net.psforever.login
import akka.actor.MDCContextAware.Implicits._
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import net.psforever.objects.Default
import net.psforever.packet._
import net.psforever.packet.control.{HandleGamePacket, _}
import org.log4s.MDC
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration._
/**
* In between the network side and the higher functioning side of the simulation:
* accept packets and transform them into a sequence of data (encoding), and
* accept a sequence of data and transform it into s packet (decoding).<br>
* <br>
* Following the standardization of the `SessionRouter` pipeline, the throughput of this `Actor` has directionality.
* The "network," where the encoded data comes and goes, is assumed to be `leftRef`.
* The "simulation", where the decoded packets come and go, is assumed to be `rightRef`.
* `rightRef` can accept a sequence that looks like encoded data but it will merely pass out the same sequence.
* Likewise, `leftRef` accepts decoded packets but merely ejects the same packets without doing any work on them.
* The former functionality is anticipated.
* The latter functionality is deprecated.<br>
* <br>
* Encoded data leaving the `Actor` (`leftRef`) is limited by an upper bound capacity.
* Sequences can not be larger than that bound or else they will be dropped.
* This maximum transmission unit (MTU) is used to divide the encoded sequence into chunks of encoded data,
* re-packaged into nested `ControlPacket` units, and each unit encoded.
* The outer packaging is numerically consistent with a `subslot` that starts counting once the simulation starts.
* The client is very specific about the `subslot` number and will reject out-of-order packets.
* It resets to 0 each time this `Actor` starts up and the client reflects this functionality.
*/
class PacketCodingActor extends Actor with MDCContextAware {
private var sessionId: Long = 0
private var subslotOutbound: Int = 0
private var subslotInbound: Int = 0
private var leftRef: ActorRef = ActorRef.noSender
private var rightRef: ActorRef = ActorRef.noSender
private[this] val log = org.log4s.getLogger
/*
Since the client can indicate missing packets when sending SlottedMetaPackets we should keep a history of them to resend to the client when requested with a RelatedA packet
Since the subslot counter can wrap around, we need to use a LinkedHashMap to maintain the order packets are inserted, then we can drop older entries as required
For example when a RelatedB packet arrives we can remove any entries to the left of the received ones without risking removing newer entries if the subslot counter wraps around back to 0
*/
private var slottedPacketLog: mutable.LinkedHashMap[Int, ByteVector] = mutable.LinkedHashMap()
// Due to the fact the client can send `RelatedA` packets out of order, we need to keep a buffer of which subslots arrived correctly, order them
// and then act accordingly to send the missing subslot packet after a specified timeout
private var relatedALog: ArrayBuffer[Int] = ArrayBuffer()
private var relatedABufferTimeout: Cancellable = Default.Cancellable
def AddSlottedPacketToLog(subslot: Int, packet: ByteVector): Unit = {
val log_limit = 500 // Number of SlottedMetaPackets to keep in history
if (slottedPacketLog.size > log_limit) {
slottedPacketLog = slottedPacketLog.drop(slottedPacketLog.size - log_limit)
}
slottedPacketLog { subslot } = packet
}
override def postStop() = {
subslotOutbound = 0 //in case this `Actor` restarts
super.postStop()
}
def receive = Initializing
def Initializing: Receive = {
case HelloFriend(sharedSessionId, pipe) =>
import MDCContextAware.Implicits._
this.sessionId = sharedSessionId
leftRef = sender()
if (pipe.hasNext) {
rightRef = pipe.next()
rightRef !> HelloFriend(sessionId, pipe)
} else {
rightRef = sender()
}
log.trace(s"Left sender ${leftRef.path.name}")
context.become(Established)
case default =>
log.error("Unknown message " + default)
context.stop(self)
}
def Established: Receive = {
case PacketCodingActor.SubslotResend() => {
log.trace(s"Subslot resend timeout reached, session: ${sessionId}")
relatedABufferTimeout.cancel()
log.trace(s"Client indicated successful subslots ${relatedALog.sortBy(x => x).mkString(" ")}")
// If a non-contiguous range of RelatedA packets were received we may need to send multiple missing packets, thus split the array into contiguous ranges
val sorted_log = relatedALog.sortBy(x => x)
val split_logs: ArrayBuffer[ArrayBuffer[Int]] = new ArrayBuffer[ArrayBuffer[Int]]()
var curr: ArrayBuffer[Int] = ArrayBuffer()
for (i <- 0 to sorted_log.size - 1) {
if (i == 0 || (sorted_log(i) != sorted_log(i - 1) + 1)) {
curr = new ArrayBuffer()
split_logs.append(curr)
}
curr.append(sorted_log(i))
}
if (split_logs.size > 1) log.trace(s"Split successful subslots into ${split_logs.size} contiguous chunks")
for (range <- split_logs) {
log.trace(s"Processing chunk ${range.mkString(" ")}")
val first_accepted_subslot = range.min
val missing_subslot = first_accepted_subslot - 1
slottedPacketLog.get(missing_subslot) match {
case Some(packet: ByteVector) =>
log.info(s"Resending packet with subslot: $missing_subslot to session: ${sessionId}")
sendResponseLeft(packet)
case None =>
log.error(s"Couldn't find packet with subslot: ${missing_subslot} to resend to session ${sessionId}.")
}
}
relatedALog.clear()
}
case RawPacket(msg) =>
if (sender() == rightRef) { //from LSA, WSA, etc., to network - encode
mtuLimit(msg)
} else { //from network, to LSA, WSA, etc. - decode
UnmarshalInnerPacket(msg, "a packet")
}
//known elevated packet type
case ctrl @ ControlPacket(_, packet) =>
if (sender() == rightRef) { //from LSA, WSA, to network - encode
PacketCoding.EncodePacket(packet) match {
case Successful(data) =>
mtuLimit(data.toByteVector)
case Failure(ex) =>
log.error(s"Failed to encode a ControlPacket: $ex")
}
} else { //deprecated; ControlPackets should not be coming from this direction
log.warn(s"DEPRECATED CONTROL PACKET SEND: $ctrl")
MDC("sessionId") = sessionId.toString
handlePacketContainer(ctrl) //sendResponseRight
}
//known elevated packet type
case game @ GamePacket(_, _, packet) =>
if (sender() == rightRef) { //from LSA, WSA, etc., to network - encode
PacketCoding.EncodePacket(packet) match {
case Successful(data) =>
mtuLimit(data.toByteVector)
case Failure(ex) =>
log.error(s"Failed to encode a GamePacket: $ex")
}
} else { //deprecated; GamePackets should not be coming from this direction
log.warn(s"DEPRECATED GAME PACKET SEND: $game")
MDC("sessionId") = sessionId.toString
sendResponseRight(game)
}
//bundling packets into a SlottedMetaPacket0/MultiPacketEx
case msg @ MultiPacketBundle(list) =>
log.trace(s"BUNDLE PACKET REQUEST SEND, LEFT (always): $msg")
handleBundlePacket(list)
//etc
case msg =>
if (sender() == rightRef) {
log.trace(s"BASE CASE PACKET SEND, LEFT: $msg")
MDC("sessionId") = sessionId.toString
leftRef !> msg
} else {
log.trace(s"BASE CASE PACKET SEND, RIGHT: $msg")
MDC("sessionId") = sessionId.toString
rightRef !> msg
}
}
/**
* Retrieve the current subslot number.
* Increment the `subslot` for the next time it is needed.
* @return a `16u` number starting at 0
*/
def Subslot: Int = {
if (subslotOutbound == 65536) { //TODO what is the actual wrap number?
subslotOutbound = 0
subslotOutbound
} else {
val curr = subslotOutbound
subslotOutbound += 1
curr
}
}
/**
* Check that an outbound packet is not too big to get stuck by the MTU.
* If it is larger than the MTU, divide it up and re-package the sections.
* Otherwise, send the data out like normal.
* @param msg the encoded packet data
*/
def mtuLimit(msg: ByteVector): Unit = {
if (msg.length > PacketCodingActor.MTU_LIMIT_BYTES) {
handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(msg)))
} else {
sendResponseLeft(msg)
}
}
/**
* Transform a `ControlPacket` into `ByteVector` data for splitting.
* @param cont the original `ControlPacket`
*/
def handleSplitPacket(cont: ControlPacket): Unit = {
PacketCoding.getPacketDataForEncryption(cont) match {
case Successful((_, data)) =>
handleSplitPacket(data)
case Failure(ex) =>
log.error(s"$ex")
}
}
/**
* Accept `ByteVector` data, representing a `ControlPacket`, and split it into chunks.
* The chunks should not be blocked by the MTU.
* Send each chunk (towards the network) as it is converted.
* @param data `ByteVector` data to be split
*/
def handleSplitPacket(data: ByteVector): Unit = {
val lim = PacketCodingActor.MTU_LIMIT_BYTES - 4 //4 bytes is the base size of SlottedMetaPacket
data
.grouped(lim)
.foreach(bvec => {
val subslot = Subslot
PacketCoding.EncodePacket(SlottedMetaPacket(4, subslot, bvec)) match {
case Successful(bdata) =>
AddSlottedPacketToLog(subslot, bdata.toByteVector)
sendResponseLeft(bdata.toByteVector)
case f: Failure =>
log.error(s"$f")
}
})
}
/**
* Accept a `List` of packets and sequentially re-package the elements from the list into multiple container packets.<br>
* <br>
* The original packets are encoded then paired with their encoding lengths plus extra space to prefix the length.
* Encodings from these pairs are drawn from the list until into buckets that fit a maximum byte stream length.
* The size limitation on any bucket is the MTU limit.
* less by the base sizes of `MultiPacketEx` (2) and of `SlottedMetaPacket` (4).
* @param bundle the packets to be bundled
*/
def handleBundlePacket(bundle: List[PlanetSidePacket]): Unit = {
val packets: List[ByteVector] = recursiveEncode(bundle.iterator)
recursiveFillPacketBuckets(packets.iterator, PacketCodingActor.MTU_LIMIT_BYTES - 6)
.foreach(list => {
handleBundlePacket(list.toVector)
})
}
/**
* Accept a `Vector` of encoded packets and re-package them.
* The normal order is to package the elements of the vector into a `MultiPacketEx`.
* If the vector only has one element, it will get packaged by itself in a `SlottedMetaPacket`.
* If that one element risks being too big for the MTU, however, it will be handled off to be split.
* Splitting should preserve `Subslot` ordering with the rest of the bundling.
* @param vec a specific number of byte streams
*/
def handleBundlePacket(vec: Vector[ByteVector]): Unit = {
if (vec.size == 1) {
val elem = vec.head
if (elem.length > PacketCodingActor.MTU_LIMIT_BYTES - 4) {
handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(elem)))
} else {
handleBundlePacket(elem)
}
} else {
PacketCoding.EncodePacket(MultiPacketEx(vec)) match {
case Successful(bdata) =>
handleBundlePacket(bdata.toByteVector)
case Failure(e) =>
log.warn(s"bundling failed on MultiPacketEx creation: - $e")
}
}
}
/**
* Accept `ByteVector` data and package it into a `SlottedMetaPacket`.
* Send it (towards the network) upon successful encoding.
* @param data an encoded packet
*/
def handleBundlePacket(data: ByteVector): Unit = {
val subslot = Subslot
PacketCoding.EncodePacket(SlottedMetaPacket(0, subslot, data)) match {
case Successful(bdata) =>
AddSlottedPacketToLog(subslot, bdata.toByteVector)
sendResponseLeft(bdata.toByteVector)
case Failure(e) =>
log.warn(s"bundling failed on SlottedMetaPacket creation: - $e")
}
}
/**
* Encoded sequence of data going towards the network.
* @param cont the data
*/
def sendResponseLeft(cont: ByteVector): Unit = {
log.trace("PACKET SEND, LEFT: " + cont)
MDC("sessionId") = sessionId.toString
leftRef !> RawPacket(cont)
}
/**
* Transform data into a container packet and re-submit that container to the process that handles the packet.
* @param data the packet data
* @param description an explanation of the input `data`
*/
def UnmarshalInnerPacket(data: ByteVector, description: String): Unit = {
PacketCoding.unmarshalPayload(0, data) match { //TODO is it safe for this to always be 0?
case Successful(packet) =>
handlePacketContainer(packet)
case Failure(ex) =>
log.info(s"Failed to unmarshal $description: $ex. Data : $data")
}
}
/**
* Sort and redirect a container packet bound for the server by type of contents.
* `GamePacket` objects can just onwards without issue.
* `ControlPacket` objects may need to be dequeued.
* All other container types are invalid.
* @param container the container packet
*/
def handlePacketContainer(container: PlanetSidePacketContainer): Unit = {
container match {
case _: GamePacket =>
sendResponseRight(container)
case ControlPacket(_, ctrlPkt) =>
handleControlPacket(container, ctrlPkt)
case default =>
log.warn(s"Invalid packet container class received: ${default.getClass.getName}") //do not spill contents in log
}
}
/**
* Process a control packet or determine that it does not need to be processed at this level.
* Primarily, if the packet is of a type that contains another packet that needs be be unmarshalled,
* that/those packet must be unwound.<br>
* <br>
* The subslot information is used to identify these nested packets after arriving at their destination,
* to establish order for sequential packets and relation between divided packets.
* @param container the original container packet
* @param packet the packet that was extracted from the container
*/
def handleControlPacket(container: PlanetSidePacketContainer, packet: PlanetSideControlPacket) = {
packet match {
case SlottedMetaPacket(slot, subslot, innerPacket) =>
subslotInbound = subslot
self.tell(PacketCoding.CreateControlPacket(RelatedB(slot, subslot)), rightRef) //will go to the network
UnmarshalInnerPacket(innerPacket, "the inner packet of a SlottedMetaPacket")
case MultiPacket(packets) =>
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacket") }
case MultiPacketEx(packets) =>
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacketEx") }
case RelatedA(slot, subslot) =>
log.trace(s"Client indicated a packet is missing prior to slot: $slot subslot: $subslot, session: ${sessionId}")
relatedALog += subslot
// (re)start the timeout period, if no more RelatedA packets are sent before the timeout period elapses the missing packet(s) will be resent
import scala.concurrent.ExecutionContext.Implicits.global
relatedABufferTimeout.cancel()
relatedABufferTimeout =
context.system.scheduler.scheduleOnce(100 milliseconds, self, PacketCodingActor.SubslotResend())
case RelatedB(slot, subslot) =>
log.trace(s"result $slot: subslot $subslot accepted, session: ${sessionId}")
// The client has indicated it's received up to a certain subslot, that means we can purge the log of any subslots prior to and including the confirmed subslot
// Find where this subslot is stored in the packet log (if at all) and drop anything to the left of it, including itself
if (relatedABufferTimeout.isCancelled || relatedABufferTimeout == Default.Cancellable) {
val pos = slottedPacketLog.keySet.toArray.indexOf(subslot)
if (pos != -1) {
slottedPacketLog = slottedPacketLog.drop(pos + 1)
log.trace(s"Subslots left in log: ${slottedPacketLog.keySet.toString()}")
}
}
case _ =>
sendResponseRight(container)
}
}
/**
* Decoded packet going towards the simulation.
* @param cont the packet
*/
def sendResponseRight(cont: PlanetSidePacketContainer): Unit = {
log.trace("PACKET SEND, RIGHT: " + cont)
MDC("sessionId") = sessionId.toString
rightRef !> cont
}
/**
* Accept a series of packets and transform it into a series of packet encodings.
* Packets that do not encode properly are simply excluded from the product.
* This is not treated as an error or exception; a warning will merely be logged.
* @param iter the `Iterator` for a series of packets
* @param out updated series of byte stream data produced through successful packet encoding;
* defaults to an empty list
* @return a series of byte stream data produced through successful packet encoding
*/
@tailrec private def recursiveEncode(
iter: Iterator[PlanetSidePacket],
out: List[ByteVector] = List()
): List[ByteVector] = {
if (!iter.hasNext) {
out
} else {
import net.psforever.packet.{PlanetSideControlPacket, PlanetSideGamePacket}
iter.next() match {
case msg: PlanetSideGamePacket =>
PacketCoding.EncodePacket(msg) match {
case Successful(bytecode) =>
recursiveEncode(iter, out :+ bytecode.toByteVector)
case Failure(e) =>
log.warn(s"game packet $msg, part of a bundle, did not encode - $e")
recursiveEncode(iter, out)
}
case msg: PlanetSideControlPacket =>
PacketCoding.EncodePacket(msg) match {
case Successful(bytecode) =>
recursiveEncode(iter, out :+ bytecode.toByteVector)
case Failure(e) =>
log.warn(s"control packet $msg, part of a bundle, did not encode - $e")
recursiveEncode(iter, out)
}
case _ =>
recursiveEncode(iter, out)
}
}
}
/**
* Accept a series of byte stream data and sort into sequential size-limited buckets of the same byte streams.
* Note that elements that exceed `lim` by themselves are always sorted into their own buckets.
* @param iter an `Iterator` of a series of byte stream data
* @param lim the maximum stream length permitted
* @param curr the stream length of the current bucket
* @param out updated series of byte stream data stored in buckets
* @return a series of byte stream data stored in buckets
*/
@tailrec private def recursiveFillPacketBuckets(
iter: Iterator[ByteVector],
lim: Int,
curr: Int = 0,
out: List[mutable.ListBuffer[ByteVector]] = List(mutable.ListBuffer())
): List[mutable.ListBuffer[ByteVector]] = {
if (!iter.hasNext) {
out
} else {
val data = iter.next()
var len = data.length.toInt
len = len + (if (len < 256) { 1 }
else if (len < 65536) { 2 }
else { 4 }) //space for the prefixed length byte(s)
if (curr + len > lim && out.last.nonEmpty) { //bucket must have something in it before swapping
recursiveFillPacketBuckets(iter, lim, len, out :+ mutable.ListBuffer(data))
} else {
out.last += data
recursiveFillPacketBuckets(iter, lim, curr + len, out)
}
}
}
}
object PacketCodingActor {
final val MTU_LIMIT_BYTES: Int = 467
private final case class SubslotResend()
}

View file

@ -0,0 +1,103 @@
package net.psforever.login
import java.net.InetSocketAddress
import akka.actor.MDCContextAware.Implicits._
import akka.actor.{ActorContext, ActorRef, PoisonPill, _}
import com.github.nscala_time.time.Imports._
import scodec.bits._
sealed trait SessionState
final case class New() extends SessionState
final case class Related() extends SessionState
final case class Handshaking() extends SessionState
final case class Established() extends SessionState
final case class Closing() extends SessionState
final case class Closed() extends SessionState
class Session(
val sessionId: Long,
val socketAddress: InetSocketAddress,
returnActor: ActorRef,
sessionPipeline: List[SessionPipeline]
)(
implicit val context: ActorContext,
implicit val self: ActorRef
) {
var state: SessionState = New()
val sessionCreatedTime: DateTime = DateTime.now()
var sessionEndedTime: DateTime = DateTime.now()
val pipeline = sessionPipeline.map { actor =>
val a = context.actorOf(actor.props, actor.nameTemplate + sessionId.toString)
context.watch(a)
a
}
val pipelineIter = pipeline.iterator
if (pipelineIter.hasNext) {
pipelineIter.next() ! HelloFriend(sessionId, pipelineIter)
}
// statistics
var bytesSent: Long = 0
var bytesReceived: Long = 0
var inboundPackets: Long = 0
var outboundPackets: Long = 0
var lastInboundEvent: Long = System.nanoTime()
var lastOutboundEvent: Long = System.nanoTime()
var inboundPacketRate: Double = 0.0
var outboundPacketRate: Double = 0.0
var inboundBytesPerSecond: Double = 0.0
var outboundBytesPerSecond: Double = 0.0
def receive(packet: RawPacket): Unit = {
bytesReceived += packet.data.size
inboundPackets += 1
lastInboundEvent = System.nanoTime()
pipeline.head !> packet
}
def send(packet: ByteVector): Unit = {
bytesSent += packet.size
outboundPackets += 1
lastOutboundEvent = System.nanoTime()
returnActor ! SendPacket(packet, socketAddress)
}
def dropSession(graceful: Boolean) = {
pipeline.foreach(context.unwatch)
pipeline.foreach(_ ! PoisonPill)
sessionEndedTime = DateTime.now()
setState(Closed())
}
def getState = state
def setState(newState: SessionState): Unit = {
state = newState
}
def getPipeline: List[ActorRef] = pipeline
def getTotalBytes = {
bytesSent + bytesReceived
}
def timeSinceLastInboundEvent = {
(System.nanoTime() - lastInboundEvent) / 1000000
}
def timeSinceLastOutboundEvent = {
(System.nanoTime() - lastOutboundEvent) / 1000000
}
override def toString: String = {
s"Session($sessionId, $getTotalBytes)"
}
}

View file

@ -0,0 +1,198 @@
package net.psforever.login
import java.net.InetSocketAddress
import akka.actor.SupervisorStrategy.Stop
import akka.actor._
import net.psforever.packet.PacketCoding
import net.psforever.packet.control.ConnectionClose
import net.psforever.util.Config
import org.log4s.MDC
import scodec.bits._
import net.psforever.services.ServiceManager
import net.psforever.services.ServiceManager.Lookup
import net.psforever.services.account.{IPAddress, StoreIPAddress}
import scala.collection.mutable
import scala.concurrent.duration._
sealed trait SessionRouterAPI
final case class RawPacket(data: ByteVector) extends SessionRouterAPI
final case class ResponsePacket(data: ByteVector) extends SessionRouterAPI
final case class DropSession(id: Long, reason: String) extends SessionRouterAPI
final case class SessionReaper() extends SessionRouterAPI
case class SessionPipeline(nameTemplate: String, props: Props)
/**
* Login sessions are divided between two actors. The crypto session actor transparently handles all of the cryptographic
* setup of the connection. Once a correct crypto session has been established, all packets, after being decrypted
* will be passed on to the login session actor. This actor has important state that is used to maintain the login
* session.
*
* > PlanetSide Session Pipeline <
*
* read() route decrypt
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
* /|\ | /|\ | /|\ |
* | write() | | encrypt | | response |
* +--------------+ +-----------+ +-----------------+
*/
class SessionRouter(role: String, pipeline: List[SessionPipeline]) extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger(self.path.name)
import scala.concurrent.ExecutionContext.Implicits.global
val sessionReaper = context.system.scheduler.scheduleWithFixedDelay(10 seconds, 5 seconds, self, SessionReaper())
val idBySocket = mutable.Map[InetSocketAddress, Long]()
val sessionById = mutable.Map[Long, Session]()
val sessionByActor = mutable.Map[ActorRef, Session]()
val closePacket = PacketCoding.EncodePacket(ConnectionClose()).require.bytes
var accountIntermediary: ActorRef = ActorRef.noSender
var sessionId = 0L // this is a connection session, not an actual logged in session ID
var inputRef: ActorRef = ActorRef.noSender
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
override def preStart() = {
log.info(s"SessionRouter (for ${role}s) initializing ...")
}
def receive = initializing
def initializing: Receive = {
case Hello() =>
inputRef = sender()
ServiceManager.serviceManager ! Lookup("accountIntermediary")
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
log.info(s"SessionRouter starting; ready for $role sessions")
context.become(started)
case default =>
log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...")
context.stop(self)
}
override def postStop() = {
sessionReaper.cancel()
}
def started: Receive = {
case _ @ReceivedPacket(msg, from) =>
var session: Session = null
if (!idBySocket.contains(from)) {
session = createNewSession(from)
} else {
val id = idBySocket { from }
session = sessionById { id }
}
if (session.state != Closed()) {
MDC("sessionId") = session.sessionId.toString
log.trace(s"RECV: $msg -> ${session.getPipeline.head.path.name}")
session.receive(RawPacket(msg))
MDC.clear()
}
case ResponsePacket(msg) =>
val session = sessionByActor.get(sender())
if (session.isDefined) {
if (session.get.state != Closed()) {
MDC("sessionId") = session.get.sessionId.toString
log.trace(s"SEND: $msg -> ${inputRef.path.name}")
session.get.send(msg)
MDC.clear()
}
} else {
log.error("Dropped old response packet from actor " + sender().path.name)
}
case DropSession(id, reason) =>
val session = sessionById.get(id)
if (session.isDefined) {
removeSessionById(id, reason, graceful = true)
} else {
log.error(s"Requested to drop non-existent session ID=$id from ${sender()}")
}
case SessionReaper() =>
val inboundGrace = Config.app.network.session.inboundGraceTime.toMillis
val outboundGrace = Config.app.network.session.outboundGraceTime.toMillis
sessionById.foreach {
case (id, session) =>
log.trace(session.toString)
if (session.getState == Closed()) {
// clear mappings
session.getPipeline.foreach(sessionByActor remove)
sessionById.remove(id)
idBySocket.remove(session.socketAddress)
log.debug(s"Reaped session ID=$id")
} else if (session.timeSinceLastInboundEvent > inboundGrace) {
removeSessionById(id, "session timed out (inbound)", graceful = false)
} else if (session.timeSinceLastOutboundEvent > outboundGrace) {
removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU
}
}
case Terminated(actor) =>
val terminatedSession = sessionByActor.get(actor)
if (terminatedSession.isDefined) {
removeSessionById(terminatedSession.get.sessionId, s"${actor.path.name} died", graceful = true)
} else {
log.error("Received an invalid actor Termination from " + actor.path.name)
}
case default =>
log.error(s"Unknown message $default from " + sender().path)
}
def createNewSession(address: InetSocketAddress) = {
val id = newSessionId
val session = new Session(id, address, inputRef, pipeline)
// establish mappings for easy lookup
idBySocket { address } = id
sessionById { id } = session
session.getPipeline.foreach { actor =>
sessionByActor { actor } = session
}
log.info(s"New session ID=$id from " + address.toString)
if (role == "Login") {
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
}
session
}
def removeSessionById(id: Long, reason: String, graceful: Boolean): Unit = {
val sessionOption = sessionById.get(id)
if (sessionOption.isEmpty)
return
val session: Session = sessionOption.get
if (graceful) {
for (_ <- 0 to 5) {
session.send(closePacket)
}
}
// kill all session specific actors
session.dropSession(graceful)
log.info(s"Dropping session ID=$id (reason: $reason)")
}
def newSessionId = {
val oldId = sessionId
sessionId += 1
oldId
}
}

View file

@ -0,0 +1,50 @@
package net.psforever.login
import java.net.{InetAddress, InetSocketAddress}
import akka.actor.SupervisorStrategy.Stop
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props}
import akka.io._
class TcpListener[T <: Actor](actorClass: Class[T], nextActorName: String, listenAddress: InetAddress, port: Int)
extends Actor {
private val log = org.log4s.getLogger(self.path.name)
override def supervisorStrategy =
OneForOneStrategy() {
case _ => Stop
}
import context.system
IO(Tcp) ! Tcp.Bind(self, new InetSocketAddress(listenAddress, port))
var sessionId = 0L
var bytesRecevied = 0L
var bytesSent = 0L
var nextActor: ActorRef = ActorRef.noSender
def receive = {
case Tcp.Bound(local) =>
log.info(s"Now listening on TCP:$local")
context.become(ready(sender()))
case Tcp.CommandFailed(Tcp.Bind(_, address, _, _, _)) =>
log.error("Failed to bind to the network interface: " + address)
context.system.terminate()
case default =>
log.error(s"Unexpected message $default")
}
def ready(socket: ActorRef): Receive = {
case Tcp.Connected(remote, local) =>
val connection = sender()
val session = sessionId
val handler = context.actorOf(Props(actorClass, remote, connection), nextActorName + session)
connection ! Tcp.Register(handler)
sessionId += 1
case Tcp.Unbind => socket ! Tcp.Unbind
case Tcp.Unbound => context.stop(self)
case default => log.error(s"Unhandled message: $default")
}
}

View file

@ -0,0 +1,79 @@
package net.psforever.login
import java.net.{InetAddress, InetSocketAddress}
import akka.actor.SupervisorStrategy.Stop
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, Terminated}
import akka.io._
import scodec.bits._
import scodec.interop.akka._
final case class ReceivedPacket(msg: ByteVector, from: InetSocketAddress)
final case class SendPacket(msg: ByteVector, to: InetSocketAddress)
final case class Hello()
final case class HelloFriend(sessionId: Long, next: Iterator[ActorRef])
class UdpListener(
nextActorProps: Props,
nextActorName: String,
listenAddress: InetAddress,
port: Int,
netParams: Option[NetworkSimulatorParameters]
) extends Actor {
private val log = org.log4s.getLogger(self.path.name)
override def supervisorStrategy =
OneForOneStrategy() {
case _ => Stop
}
import context.system
// If we have network parameters, start the network simulator
if (netParams.isDefined) {
// See http://www.cakesolutions.net/teamblogs/understanding-akkas-recommended-practice-for-actor-creation-in-scala
// For why we cant do Props(new Actor) here
val sim = context.actorOf(Props(classOf[UdpNetworkSimulator], self, netParams.get))
IO(Udp).tell(Udp.Bind(sim, new InetSocketAddress(listenAddress, port)), sim)
} else {
IO(Udp) ! Udp.Bind(self, new InetSocketAddress(listenAddress, port))
}
var bytesRecevied = 0L
var bytesSent = 0L
var nextActor: ActorRef = ActorRef.noSender
def receive = {
case Udp.Bound(local) =>
log.info(s"Now listening on UDP:$local")
createNextActor()
context.become(ready(sender()))
case Udp.CommandFailed(Udp.Bind(_, address, _)) =>
log.error("Failed to bind to the network interface: " + address)
context.system.terminate()
case default =>
log.error(s"Unexpected message $default")
}
def ready(socket: ActorRef): Receive = {
case SendPacket(msg, to) =>
bytesSent += msg.size
socket ! Udp.Send(msg.toByteString, to)
case Udp.Received(data, remote) =>
bytesRecevied += data.size
nextActor ! ReceivedPacket(data.toByteVector, remote)
case Udp.Unbind => socket ! Udp.Unbind
case Udp.Unbound => context.stop(self)
case Terminated(actor) =>
log.error(s"Next actor ${actor.path.name} has died...restarting")
createNextActor()
case default => log.error(s"Unhandled message: $default")
}
def createNextActor() = {
nextActor = context.actorOf(nextActorProps, nextActorName)
context.watch(nextActor)
nextActor ! Hello()
}
}

View file

@ -0,0 +1,146 @@
package net.psforever.login
import akka.actor.{Actor, ActorRef}
import akka.io._
import scala.collection.mutable
import scala.concurrent.duration._
import scala.util.Random
/** Parameters for the Network simulator
*
* @param packetLoss The percentage from [0.0, 1.0] that a packet will be lost
* @param packetDelay The end-to-end delay (ping) of all packets
* @param packetReorderingChance The percentage from [0.0, 1.0] that a packet will be reordered
* @param packetReorderingTime The absolute adjustment in milliseconds that a packet can have (either
* forward or backwards in time)
*/
case class NetworkSimulatorParameters(
packetLoss: Double,
packetDelay: Long,
packetReorderingChance: Double,
packetReorderingTime: Long
) {
assert(packetLoss >= 0.0 && packetLoss <= 1.0)
assert(packetDelay >= 0)
assert(packetReorderingChance >= 0.0 && packetReorderingChance <= 1.0)
assert(packetReorderingTime >= 0)
override def toString =
"NetSimParams: loss %.2f%% / delay %dms / reorder %.2f%% / reorder +/- %dms".format(
packetLoss * 100,
packetDelay,
packetReorderingChance * 100,
packetReorderingTime
)
}
class UdpNetworkSimulator(server: ActorRef, params: NetworkSimulatorParameters) extends Actor {
private val log = org.log4s.getLogger
import scala.concurrent.ExecutionContext.Implicits.global
//******* Variables
val packetDelayDuration = (params.packetDelay / 2).milliseconds
type QueueItem = (Udp.Message, Long)
// sort in ascending order (older things get dequeued first)
implicit val QueueItem = Ordering.by[QueueItem, Long](_._2).reverse
val inPacketQueue = mutable.PriorityQueue[QueueItem]()
val outPacketQueue = mutable.PriorityQueue[QueueItem]()
val chaos = new Random()
var interface = ActorRef.noSender
def receive = {
case UdpNetworkSimulator.ProcessInputQueue() =>
val time = System.nanoTime()
var exit = false
while (inPacketQueue.nonEmpty && !exit) {
val lastTime = time - inPacketQueue.head._2
// this packet needs to be sent within 20 milliseconds or more
if (lastTime >= 20000000) {
server.tell(inPacketQueue.dequeue()._1, interface)
} else {
schedule(lastTime.nanoseconds, outbound = false)
exit = true
}
}
case UdpNetworkSimulator.ProcessOutputQueue() =>
val time = System.nanoTime()
var exit = false
while (outPacketQueue.nonEmpty && !exit) {
val lastTime = time - outPacketQueue.head._2
// this packet needs to be sent within 20 milliseconds or more
if (lastTime >= 20000000) {
interface.tell(outPacketQueue.dequeue()._1, server)
} else {
schedule(lastTime.nanoseconds, outbound = true)
exit = true
}
}
// outbound messages
case msg @ Udp.Send(payload, target, _) =>
handlePacket(msg, outPacketQueue, outbound = true)
// inbound messages
case msg @ Udp.Received(payload, sender) =>
handlePacket(msg, inPacketQueue, outbound = false)
case msg @ Udp.Bound(address) =>
interface = sender()
log.info(s"Hooked ${server.path} for network simulation")
server.tell(msg, self) // make sure the server sends *us* the packets
case default =>
val from = sender()
if (from == server)
interface.tell(default, server)
else if (from == interface)
server.tell(default, interface)
else
log.error("Unexpected sending Actor " + from.path)
}
def handlePacket(message: Udp.Message, queue: mutable.PriorityQueue[QueueItem], outbound: Boolean) = {
val name: String = if (outbound) "OUT" else "IN"
val queue: mutable.PriorityQueue[QueueItem] = if (outbound) outPacketQueue else inPacketQueue
if (chaos.nextDouble() > params.packetLoss) {
// if the message queue is empty, then we need to reschedule our task
if (queue.isEmpty)
schedule(packetDelayDuration, outbound)
// perform a reordering
if (chaos.nextDouble() <= params.packetReorderingChance) {
// creates the range (-1.0, 1.0)
// time adjustment to move the packet (forward or backwards in time)
val adj = (2 * (chaos.nextDouble() - 0.5) * params.packetReorderingTime).toLong
queue += ((message, System.nanoTime() + adj * 1000000))
log.debug(s"Reordered $name by ${adj}ms - $message")
} else { // normal message
queue += ((message, System.nanoTime()))
}
} else {
log.debug(s"Dropped $name - $message")
}
}
def schedule(duration: FiniteDuration, outbound: Boolean) =
context.system.scheduler.scheduleOnce(
packetDelayDuration,
self,
if (outbound) UdpNetworkSimulator.ProcessOutputQueue() else UdpNetworkSimulator.ProcessInputQueue()
)
}
object UdpNetworkSimulator {
//******* Internal messages
private final case class ProcessInputQueue()
private final case class ProcessOutputQueue()
}

View file

@ -0,0 +1,651 @@
package net.psforever.login
import akka.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout
import net.psforever.objects.equipment.{Ammo, Equipment}
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.zones.Zone
import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool}
import net.psforever.packet.game.ObjectHeldMessage
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.language.implicitConversions
import scala.util.{Failure, Success}
object WorldSession {
/**
* Convert a boolean value into an integer value.
* Use: `true:Int` or `false:Int`
*
* @param b `true` or `false` (or `null`)
* @return 1 for `true`; 0 for `false`
*/
implicit def boolToInt(b: Boolean): Int = if (b) 1 else 0
private implicit val timeout = new Timeout(5000 milliseconds)
/**
* Use this for placing equipment that has yet to be registered into a container,
* such as in support of changing ammunition types in `Tool` objects (weapons).
* If the object can not be placed into the container, it will be dropped onto the ground.
* It will also be dropped if it takes too long to be placed.
* Item swapping during the placement is not allowed.
* @see `ask`
* @see `ChangeAmmoMessage`
* @see `Containable.CanNotPutItemInSlot`
* @see `Containable.PutItemAway`
* @see `Future.onComplete`
* @see `Future.recover`
* @see `tell`
* @see `Zone.Ground.DropItem`
* @param obj the container
* @param item the item being manipulated
* @return a `Future` that anticipates the resolution to this manipulation
*/
def PutEquipmentInInventoryOrDrop(obj: PlanetSideServerObject with Container)(item: Equipment): Future[Any] = {
val localContainer = obj
val localItem = item
val result = ask(localContainer.Actor, Containable.PutItemAway(localItem))
result.onComplete {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
localContainer.Zone.Ground.tell(
Zone.Ground.DropItem(localItem, localContainer.Position, Vector3.z(localContainer.Orientation.z)),
localContainer.Actor
)
case _ => ;
}
result
}
/**
* Use this for placing equipment that has yet to be registered into a container,
* such as in support of changing ammunition types in `Tool` objects (weapons).
* Equipment will go wherever it fits in containing object, or be dropped if it fits nowhere.
* Item swapping during the placement is not allowed.
* @see `ChangeAmmoMessage`
* @see `GUIDTask.RegisterEquipment`
* @see `PutEquipmentInInventoryOrDrop`
* @see `Task`
* @see `TaskResolver.GiveTask`
* @param obj the container
* @param item the item being manipulated
* @return a `TaskResolver` object
*/
def PutNewEquipmentInInventoryOrDrop(
obj: PlanetSideServerObject with Container
)(item: Equipment): TaskResolver.GiveTask = {
val localZone = obj.Zone
TaskResolver.GiveTask(
new Task() {
private val localContainer = obj
private val localItem = item
override def isComplete: Task.Resolution.Value = Task.Resolution.Success
def Execute(resolver: ActorRef): Unit = {
PutEquipmentInInventoryOrDrop(localContainer)(localItem)
resolver ! Success(this)
}
},
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
)
}
/**
* Use this for obtaining new equipment from a loadout specification.
* The loadout specification contains a specific slot position for placing the item.
* This request will (probably) be coincidental with a number of other such requests based on that loadout
* so items must be rigidly placed else cascade into a chaostic order.
* Item swapping during the placement is not allowed.
* @see `ask`
* @see `AvatarAction.ObjectDelete`
* @see `ChangeAmmoMessage`
* @see `Containable.CanNotPutItemInSlot`
* @see `Containable.PutItemAway`
* @see `Future.onComplete`
* @see `Future.recover`
* @see `GUIDTask.UnregisterEquipment`
* @see `tell`
* @see `Zone.AvatarEvents`
* @param obj the container
* @param taskResolver na
* @param item the item being manipulated
* @param slot na
* @return a `Future` that anticipates the resolution to this manipulation
*/
def PutEquipmentInInventorySlot(
obj: PlanetSideServerObject with Container,
taskResolver: ActorRef
)(item: Equipment, slot: Int): Future[Any] = {
val localContainer = obj
val localItem = item
val localResolver = taskResolver
val result = ask(localContainer.Actor, Containable.PutItemInSlotOnly(localItem, slot))
result.onComplete {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
case _ => ;
}
result
}
/**
* Use this for obtaining new equipment from a loadout specification.
* The loadout specification contains a specific slot position for placing the item.
* This request will (probably) be coincidental with a number of other such requests based on that loadout
* so items must be rigidly placed else cascade into a chaostic order.
* Item swapping during the placement is not allowed.
* @see `GUIDTask.RegisterEquipment`
* @see `PutEquipmentInInventorySlot`
* @see `Task`
* @see `TaskResolver.GiveTask`
* @param obj the container
* @param taskResolver na
* @param item the item being manipulated
* @param slot where the item will be placed in the container
* @return a `TaskResolver` object
*/
def PutLoadoutEquipmentInInventory(
obj: PlanetSideServerObject with Container,
taskResolver: ActorRef
)(item: Equipment, slot: Int): TaskResolver.GiveTask = {
val localZone = obj.Zone
TaskResolver.GiveTask(
new Task() {
private val localContainer = obj
private val localItem = item
private val localSlot = slot
private val localFunc: (Equipment, Int) => Future[Any] = PutEquipmentInInventorySlot(obj, taskResolver)
override def Timeout: Long = 1000
override def isComplete: Task.Resolution.Value = {
if (localItem.HasGUID && localContainer.Find(localItem).nonEmpty)
Task.Resolution.Success
else
Task.Resolution.Incomplete
}
override def Description: String = s"PutEquipmentInInventorySlot - ${localItem.Definition.Name}"
def Execute(resolver: ActorRef): Unit = {
localFunc(localItem, localSlot)
resolver ! Success(this)
}
},
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
)
}
/**
* Used for purchasing new equipment from a terminal and placing it somewhere in a player's loadout.
* Two levels of query are performed here based on the behavior expected of the item.
* First, an attempt is made to place the item anywhere in the target container as long as it does not cause swap items to be generated.
* Second, if it fails admission to the target container, an attempt is made to place it into the target player's free hand.
* If the container and the suggested player are the same, it will skip the second attempt.
* As a terminal operation, the player must receive a report regarding whether the transaction was successful.
* @see `ask`
* @see `Containable.CanNotPutItemInSlot`
* @see `Containable.PutItemInSlotOnly`
* @see `GUIDTask.RegisterEquipment`
* @see `GUIDTask.UnregisterEquipment`
* @see `Future.onComplete`
* @see `PutEquipmentInInventorySlot`
* @see `TerminalMessageOnTimeout`
* @param obj the container
* @param taskResolver na
* @param player na
* @param term na
* @param item the item being manipulated
* @return a `TaskResolver` object
*/
def BuyNewEquipmentPutInInventory(
obj: PlanetSideServerObject with Container,
taskResolver: ActorRef,
player: Player,
term: PlanetSideGUID
)(item: Equipment): TaskResolver.GiveTask = {
val localZone = obj.Zone
TaskResolver.GiveTask(
new Task() {
private val localContainer = obj
private val localItem = item
private val localPlayer = player
private val localResolver = taskResolver
private val localTermMsg: Boolean => Unit = TerminalResult(term, localPlayer, TransactionType.Buy)
override def Timeout: Long = 1000
override def isComplete: Task.Resolution.Value = {
if (localItem.HasGUID && localContainer.Find(localItem).nonEmpty)
Task.Resolution.Success
else
Task.Resolution.Incomplete
}
def Execute(resolver: ActorRef): Unit = {
TerminalMessageOnTimeout(
ask(localContainer.Actor, Containable.PutItemAway(localItem)),
localTermMsg
)
.onComplete {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
if (localContainer != localPlayer) {
TerminalMessageOnTimeout(
PutEquipmentInInventorySlot(localPlayer, localResolver)(localItem, Player.FreeHandSlot),
localTermMsg
)
.onComplete {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
localTermMsg(false)
case _ =>
localTermMsg(true)
}
} else {
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
localTermMsg(false)
}
case _ =>
localTermMsg(true)
}
resolver ! Success(this)
}
},
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
)
}
/**
* The primary use is to register new mechanized assault exo-suit armaments,
* place the newly registered weapon in hand,
* and then raise that hand (draw that slot) so that the weapon is active.
* (Players in MAX suits can not manipulate their drawn slot manually.)
* In general, this can be used for any equipment that is to be equipped to a player's hand then immediately drawn.
* Do not allow the item to be (mis)placed in any available slot.
* Item swapping during the placement is not allowed and the possibility should be proactively avoided.
* @throws `RuntimeException` if slot is not a player visible slot (holsters)
* @see `ask`
* @see `AvatarAction.ObjectDelete`
* @see `AvatarAction.SendResponse`
* @see `Containable.CanNotPutItemInSlot`
* @see `Containable.PutItemInSlotOnly`
* @see `GUIDTask.RegisterEquipment`
* @see `GUIDTask.UnregisterEquipment`
* @see `Future.onComplete`
* @see `ObjectHeldMessage`
* @see `Player.DrawnSlot`
* @see `Player.LastDrawnSlot`
* @see `Service.defaultPlayerGUID`
* @see `TaskResolver.GiveTask`
* @see `Zone.AvatarEvents`
* @param player the player whose visible slot will be equipped and drawn
* @param taskResolver na
* @param item the item to equip
* @param slot the slot in which the item will be equipped
* @return a `TaskResolver` object
*/
def HoldNewEquipmentUp(player: Player, taskResolver: ActorRef)(item: Equipment, slot: Int): TaskResolver.GiveTask = {
if (player.VisibleSlots.contains(slot)) {
val localZone = player.Zone
TaskResolver.GiveTask(
new Task() {
private val localPlayer = player
private val localGUID = player.GUID
private val localItem = item
private val localSlot = slot
private val localResolver = taskResolver
override def Timeout: Long = 1000
override def isComplete: Task.Resolution.Value = {
if (localPlayer.DrawnSlot == localSlot)
Task.Resolution.Success
else
Task.Resolution.Incomplete
}
def Execute(resolver: ActorRef): Unit = {
ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot))
.onComplete {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localZone.GUID)
case _ =>
if (localPlayer.DrawnSlot != Player.HandsDownSlot) {
localPlayer.DrawnSlot = Player.HandsDownSlot
localZone.AvatarEvents ! AvatarServiceMessage(
localPlayer.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectHeldMessage(localGUID, Player.HandsDownSlot, false)
)
)
localZone.AvatarEvents ! AvatarServiceMessage(
localZone.id,
AvatarAction.ObjectHeld(localGUID, localPlayer.LastDrawnSlot)
)
}
localPlayer.DrawnSlot = localSlot
localZone.AvatarEvents ! AvatarServiceMessage(
localZone.id,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, localSlot, false))
)
}
resolver ! Success(this)
}
},
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
)
} else {
//TODO log.error
throw new RuntimeException(s"provided slot $slot is not a player visible slot (holsters)")
}
}
/**
* Get an item from the ground and put it into the given container.
* The zone in which the item is found is expected to be the same in which the container object is located.
* If the object can not be placed into the container, it is put back on the ground.
* The item that was collected off the ground, if it is placed back on the ground,
* will be positioned with respect to the container object rather than its original location.
* @see `ask`
* @see `AvatarAction.ObjectDelete`
* @see `Future.onComplete`
* @see `Zone.AvatarEvents`
* @see `Zone.Ground.CanNotPickUpItem`
* @see `Zone.Ground.ItemInHand`
* @see `Zone.Ground.PickUpItem`
* @see `PutEquipmentInInventoryOrDrop`
* @param obj the container into which the item will be placed
* @param item the item being collected from off the ground of the container's zone
* @return a `Future` that anticipates the resolution to this manipulation
*/
def PickUpEquipmentFromGround(obj: PlanetSideServerObject with Container)(item: Equipment): Future[Any] = {
val localZone = obj.Zone
val localContainer = obj
val localItem = item
val future = ask(localZone.Ground, Zone.Ground.PickupItem(item.GUID))
future.onComplete {
case Success(Zone.Ground.ItemInHand(_)) =>
PutEquipmentInInventoryOrDrop(localContainer)(localItem)
case Success(Zone.Ground.CanNotPickupItem(_, item_guid, _)) =>
localZone.GUID(item_guid) match {
case Some(_) => ;
case None => //acting on old data?
localZone.AvatarEvents ! AvatarServiceMessage(
localZone.id,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item_guid)
)
}
case _ => ;
}
future
}
/**
* Remove an item from a container and drop it on the ground.
* @see `ask`
* @see `AvatarAction.ObjectDelete`
* @see `Containable.ItemFromSlot`
* @see `Containable.RemoveItemFromSlot`
* @see `Future.onComplete`
* @see `Future.recover`
* @see `tell`
* @see `Zone.AvatarEvents`
* @see `Zone.Ground.DropItem`
* @param obj the container to search
* @param item the item to find and remove from the container
* @param pos an optional position where to drop the item on the ground;
* expected override from original container's position
* @return a `Future` that anticipates the resolution to this manipulation
*/
def DropEquipmentFromInventory(
obj: PlanetSideServerObject with Container
)(item: Equipment, pos: Option[Vector3] = None): Future[Any] = {
val localContainer = obj
val localItem = item
val localPos = pos
val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem))
result.onComplete {
case Success(Containable.ItemFromSlot(_, Some(_), Some(_))) =>
localContainer.Zone.Ground.tell(
Zone.Ground
.DropItem(localItem, localPos.getOrElse(localContainer.Position), Vector3.z(localContainer.Orientation.z)),
localContainer.Actor
)
case _ => ;
}
result
}
/**
* Remove an item from a container and delete it.
* @see `ask`
* @see `AvatarAction.ObjectDelete`
* @see `Containable.ItemFromSlot`
* @see `Containable.RemoveItemFromSlot`
* @see `Future.onComplete`
* @see `Future.recover`
* @see `GUIDTask.UnregisterEquipment`
* @see `Zone.AvatarEvents`
* @param obj the container to search
* @param taskResolver na
* @param item the item to find and remove from the container
* @return a `Future` that anticipates the resolution to this manipulation
*/
def RemoveOldEquipmentFromInventory(obj: PlanetSideServerObject with Container, taskResolver: ActorRef)(
item: Equipment
): Future[Any] = {
val localContainer = obj
val localItem = item
val localResolver = taskResolver
val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem))
result.onComplete {
case Success(Containable.ItemFromSlot(_, Some(_), Some(_))) =>
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
case _ =>
}
result
}
/**
* Primarily, remove an item from a container and delete it.
* As a terminal operation, the player must receive a report regarding whether the transaction was successful.
* At the end of a successful transaction, and only a successful transaction,
* the item that was removed is no longer considered a valid game object.
* Contrasting `RemoveOldEquipmentFromInventory` which identifies the actual item to be eliminated,
* this function uses the slot where the item is (should be) located.
* @see `ask`
* @see `Containable.ItemFromSlot`
* @see `Containable.RemoveItemFromSlot`
* @see `Future.onComplete`
* @see `Future.recover`
* @see `GUIDTask.UnregisterEquipment`
* @see `RemoveOldEquipmentFromInventory`
* @see `TerminalMessageOnTimeout`
* @see `TerminalResult`
* @param obj the container to search
* @param taskResolver na
* @param player the player who used the terminal
* @param term the unique identifier number of the terminal
* @param slot from which slot the equipment is to be removed
* @return a `Future` that anticipates the resolution to this manipulation
*/
def SellEquipmentFromInventory(
obj: PlanetSideServerObject with Container,
taskResolver: ActorRef,
player: Player,
term: PlanetSideGUID
)(slot: Int): Future[Any] = {
val localContainer = obj
val localPlayer = player
val localSlot = slot
val localResolver = taskResolver
val localTermMsg: Boolean => Unit = TerminalResult(term, localPlayer, TransactionType.Sell)
val result = TerminalMessageOnTimeout(
ask(localContainer.Actor, Containable.RemoveItemFromSlot(localSlot)),
localTermMsg
)
result.onComplete {
case Success(Containable.ItemFromSlot(_, Some(item), Some(_))) =>
localResolver ! GUIDTask.UnregisterEquipment(item)(localContainer.Zone.GUID)
localTermMsg(true)
case _ =>
localTermMsg(false)
}
result
}
/**
* If a timeout occurs on the manipulation, declare a terminal transaction failure.
* @see `AskTimeoutException`
* @see `recover`
* @param future the item manipulation's `Future` object
* @param terminalMessage how to call the terminal message
* @return a `Future` that anticipates the resolution to this manipulation
*/
def TerminalMessageOnTimeout(future: Future[Any], terminalMessage: Boolean => Unit): Future[Any] = {
future.recover {
case _: AskTimeoutException =>
terminalMessage(false)
}
}
/**
* Announced the result of this player's terminal use, to the player that used the terminal.
* This is a necessary step for regaining terminal use which is naturally blocked by the client after a transaction request.
* @see `AvatarAction.TerminalOrderResult`
* @see `ItemTransactionResultMessage`
* @see `TransactionType`
* @param guid the terminal's unique identifier
* @param player the player who used the terminal
* @param transaction what kind of transaction was involved in terminal use
* @param result the result of that transaction
*/
def TerminalResult(guid: PlanetSideGUID, player: Player, transaction: TransactionType.Value)(
result: Boolean
): Unit = {
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(guid, transaction, result)
)
}
/**
* Drop some items on the ground is a given location.
* The location corresponds to the previous container for those items.
* @see `Zone.Ground.DropItem`
* @param container the original object that contained the items
* @param drops the items to be dropped on the ground
*/
def DropLeftovers(container: PlanetSideServerObject with Container)(drops: List[InventoryItem]): Unit = {
//drop or retire
val zone = container.Zone
val pos = container.Position
val orient = Vector3.z(container.Orientation.z)
//TODO make a sound when dropping stuff?
drops.foreach { entry => zone.Ground.tell(Zone.Ground.DropItem(entry.obj, pos, orient), container.Actor) }
}
/**
* Within a specified `Container`, find the smallest number of `Equipment` objects of a certain qualifying type
* whose sum count is greater than, or equal to, a `desiredAmount` based on an accumulator method.<br>
* <br>
* In an occupied `List` of returned `Inventory` entries, all but the last entry is typically considered "emptied."
* For objects with contained quantities, the last entry may require having that quantity be set to a non-zero number.
* @param obj the `Container` to search
* @param filterTest test used to determine inclusivity of `Equipment` collection
* @param desiredAmount how much is requested
* @param counting test used to determine value of found `Equipment`;
* defaults to one per entry
* @return a `List` of all discovered entries totaling approximately the amount requested
*/
def FindEquipmentStock(
obj: Container,
filterTest: Equipment => Boolean,
desiredAmount: Int,
counting: Equipment => Int = DefaultCount
): List[InventoryItem] = {
var currentAmount: Int = 0
obj.Inventory.Items
.filter(item => filterTest(item.obj))
.sortBy(_.start)
.takeWhile(entry => {
val previousAmount = currentAmount
currentAmount += counting(entry.obj)
previousAmount < desiredAmount
})
}
/**
* The default counting function for an item.
* Counts the number of item(s).
* @param e the `Equipment` object
* @return the quantity;
* always one
*/
def DefaultCount(e: Equipment): Int = 1
/**
* The counting function for an item of `AmmoBox`.
* Counts the `Capacity` of the ammunition.
* @param e the `Equipment` object
* @return the quantity
*/
def CountAmmunition(e: Equipment): Int = {
e match {
case a: AmmoBox => a.Capacity
case _ => 0
}
}
/**
* The counting function for an item of `Tool` where the item is also a grenade.
* Counts the number of grenades.
* @see `GlobalDefinitions.isGrenade`
* @param e the `Equipment` object
* @return the quantity
*/
def CountGrenades(e: Equipment): Int = {
e match {
case t: Tool => (GlobalDefinitions.isGrenade(t.Definition): Int) * t.Magazine
case _ => 0
}
}
/**
* Flag an `AmmoBox` object that matches for the given ammunition type.
* @param ammo the type of `Ammo` to check
* @param e the `Equipment` object
* @return `true`, if the object is an `AmmoBox` of the correct ammunition type; `false`, otherwise
*/
def FindAmmoBoxThatUses(ammo: Ammo.Value)(e: Equipment): Boolean = {
e match {
case t: AmmoBox => t.AmmoType == ammo
case _ => false
}
}
/**
* Flag a `Tool` object that matches for loading the given ammunition type.
* @param ammo the type of `Ammo` to check
* @param e the `Equipment` object
* @return `true`, if the object is a `Tool` that loads the correct ammunition type; `false`, otherwise
*/
def FindToolThatUses(ammo: Ammo.Value)(e: Equipment): Boolean = {
e match {
case t: Tool =>
t.Definition.AmmoTypes.map { _.AmmoType }.contains(ammo)
case _ =>
false
}
}
}

View file

@ -0,0 +1,30 @@
package net.psforever.login.psadmin
import net.psforever.util.Config
import scala.collection.mutable
import scala.jdk.CollectionConverters._
object CmdInternal {
def cmdDumpConfig(args: Array[String]) = {
val config =
Config.config.root.keySet.asScala.map(key => key -> Config.config.getAnyRef(key).asInstanceOf[Any]).toMap
CommandGoodResponse(s"Dump of WorldConfig", mutable.Map(config.toSeq: _*))
}
def cmdThreadDump(args: Array[String]) = {
var data = mutable.Map[String, Any]()
val traces = Thread.getAllStackTraces().asScala
var traces_fmt = List[String]()
for ((thread, trace) <- traces) {
val info = s"Thread ${thread.getId} - ${thread.getName}\n"
traces_fmt = traces_fmt ++ List(info + trace.mkString("\n"))
}
data { "trace" } = traces_fmt
CommandGoodResponse(s"Dump of ${traces.size} threads", data)
}
}

View file

@ -0,0 +1,42 @@
package net.psforever.login.psadmin
import akka.actor.typed.receptionist.Receptionist
import akka.actor.{Actor, ActorRef}
import net.psforever.services.{InterstellarClusterService, ServiceManager}
import scala.collection.mutable.Map
import akka.actor.typed.scaladsl.adapter._
class CmdListPlayers(args: Array[String], services: Map[String, ActorRef]) extends Actor {
private[this] val log = org.log4s.getLogger(self.path.name)
override def preStart() = {
ServiceManager.receptionist ! Receptionist.Find(
InterstellarClusterService.InterstellarClusterServiceKey,
context.self
)
}
override def receive = {
case InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings) =>
listings.head ! InterstellarClusterService.GetPlayers(context.self)
case InterstellarClusterService.PlayersResponse(players) =>
val data = Map[String, Any]()
data {
"player_count"
} = players.size
data {
"player_list"
} = Array[String]()
if (players.isEmpty) {
context.parent ! CommandGoodResponse("No players currently online!", data)
} else {
data {
"player_list"
} = players
context.parent ! CommandGoodResponse(s"${players.length} players online\n", data)
}
case default => log.error(s"Unexpected message $default")
}
}

View file

@ -0,0 +1,17 @@
package net.psforever.login.psadmin
import akka.actor.{Actor, ActorRef}
import scala.collection.mutable.Map
class CmdShutdown(args: Array[String], services: Map[String, ActorRef]) extends Actor {
override def preStart() = {
var data = Map[String, Any]()
context.parent ! CommandGoodResponse("Shutting down", data)
context.system.terminate()
}
override def receive = {
case default =>
}
}

View file

@ -0,0 +1,187 @@
package net.psforever.login.psadmin
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorRef, Props, Stash}
import akka.io.Tcp
import akka.util.ByteString
import org.json4s._
import org.json4s.native.Serialization.write
import scodec.bits._
import scodec.interop.akka._
import net.psforever.services.ServiceManager.Lookup
import net.psforever.services._
import scala.collection.mutable.Map
object PsAdminActor {
val whiteSpaceRegex = """\s+""".r
}
class PsAdminActor(peerAddress: InetSocketAddress, connection: ActorRef) extends Actor with Stash {
private[this] val log = org.log4s.getLogger(self.path.name)
val services = Map[String, ActorRef]()
val servicesToResolve = Array("cluster")
var buffer = ByteString()
implicit val formats = DefaultFormats // for JSON serialization
case class CommandCall(operation: String, args: Array[String])
override def preStart() = {
log.trace(s"PsAdmin connection started $peerAddress")
for (service <- servicesToResolve) {
ServiceManager.serviceManager ! Lookup(service)
}
}
override def receive = ServiceLookup
def ServiceLookup: Receive = {
case ServiceManager.LookupResult(service, endpoint) =>
services { service } = endpoint
if (services.size == servicesToResolve.size) {
unstashAll()
context.become(ReceiveCommand)
}
case default => stash()
}
def ReceiveCommand: Receive = {
case Tcp.Received(data) =>
buffer ++= data
var pos = -1;
var amount = 0
do {
pos = buffer.indexOf('\n')
if (pos != -1) {
val (cmd, rest) = buffer.splitAt(pos)
buffer = rest.drop(1); // drop the newline
// make sure the CN cant crash us
val line = cmd.decodeString("utf-8").trim
if (line != "") {
val tokens = PsAdminActor.whiteSpaceRegex.split(line)
val cmd = tokens.head
val args = tokens.tail
amount += 1
self ! CommandCall(cmd, args)
}
}
} while (pos != -1)
if (amount > 0)
context.become(ProcessCommands)
case Tcp.PeerClosed =>
context.stop(self)
case default =>
log.error(s"Unexpected message $default")
}
/// Process all buffered commands and stash other ones
def ProcessCommands: Receive = {
case c: CommandCall =>
stash()
unstashAll()
context.become(ProcessCommand)
case default =>
stash()
unstashAll()
context.become(ReceiveCommand)
}
/// Process a single command
def ProcessCommand: Receive = {
case CommandCall(cmd, args) =>
val data = Map[String, Any]()
if (cmd == "help" || cmd == "?") {
if (args.size == 0) {
var resp = "PsAdmin command usage\n"
for ((command, info) <- PsAdminCommands.commands) {
resp += s"${command} - ${info.usage}\n"
}
data { "message" } = resp
} else {
if (PsAdminCommands.commands.contains(args(0))) {
val info = PsAdminCommands.commands { args(0) }
data { "message" } = s"${args(0)} - ${info.usage}"
} else {
data { "message" } = s"Unknown command ${args(0)}"
data { "error" } = true
}
}
sendLine(write(data.toMap))
} else if (PsAdminCommands.commands.contains(cmd)) {
val cmd_template = PsAdminCommands.commands { cmd }
cmd_template match {
case PsAdminCommands.Command(usage, handler) =>
context.actorOf(Props(handler, args, services))
case PsAdminCommands.CommandInternal(usage, handler) =>
val resp = handler(args)
resp match {
case CommandGoodResponse(msg, data) =>
data { "message" } = msg
sendLine(write(data.toMap))
case CommandErrorResponse(msg, data) =>
data { "message" } = msg
data { "error" } = true
sendLine(write(data.toMap))
}
context.become(ProcessCommands)
}
} else {
data { "message" } = "Unknown command"
data { "error" } = true
sendLine(write(data.toMap))
context.become(ProcessCommands)
}
case resp: CommandResponse =>
resp match {
case CommandGoodResponse(msg, data) =>
data { "message" } = msg
sendLine(write(data.toMap))
case CommandErrorResponse(msg, data) =>
data { "message" } = msg
data { "error" } = true
sendLine(write(data.toMap))
}
context.become(ProcessCommands)
context.stop(sender())
case default =>
stash()
unstashAll()
context.become(ProcessCommands)
}
def sendLine(line: String) = {
ByteVector.encodeUtf8(line + "\n") match {
case Left(e) =>
log.error(s"Message encoding failure: $e")
case Right(bv) =>
connection ! Tcp.Write(bv.toByteString)
}
}
}

View file

@ -0,0 +1,32 @@
package net.psforever.login.psadmin
import scala.collection.mutable
sealed trait CommandResponse
case class CommandGoodResponse(message: String, data: mutable.Map[String, Any]) extends CommandResponse
case class CommandErrorResponse(message: String, data: mutable.Map[String, Any]) extends CommandResponse
object PsAdminCommands {
import CmdInternal._
val commands: Map[String, CommandInfo] = Map(
"list_players" -> Command(
"""Return a list of players connected to the interstellar cluster.""",
classOf[CmdListPlayers]
),
"dump_config" -> CommandInternal("""Dumps entire running config.""", cmdDumpConfig),
"shutdown" -> Command("""Shuts down the server forcefully.""", classOf[CmdShutdown]),
"thread_dump" -> CommandInternal("""Returns all thread's stack traces.""", cmdThreadDump)
)
sealed trait CommandInfo {
def usage: String
}
/// A command with full access to the ActorSystem and WorldServer net.psforever.services.
/// Spawns an Actor to handle the request and the service queries
case class Command[T](usage: String, handler: Class[T]) extends CommandInfo
/// A command without access to the ActorSystem or any net.psforever.services
case class CommandInternal(usage: String, handler: ((Array[String]) => CommandResponse)) extends CommandInfo
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.newcodecs
import scodec.Codec
import scodec.bits.BitVector
private[newcodecs] final class BinaryChoiceCodec[A](choice: Boolean, codec_true: => Codec[A], codec_false: => Codec[A])
extends Codec[A] {
private lazy val evaluatedCodec_true = codec_true
private lazy val evaluatedCodec_false = codec_false
override def sizeBound = if (choice) evaluatedCodec_true.sizeBound else evaluatedCodec_false.sizeBound
override def encode(a: A) = {
if (choice)
evaluatedCodec_true.encode(a)
else
evaluatedCodec_false.encode(a)
}
override def decode(buffer: BitVector) = {
if (choice)
evaluatedCodec_true.decode(buffer)
else
evaluatedCodec_false.decode(buffer)
}
override def toString =
if (choice) s"binarychoice(true, $evaluatedCodec_true, ?)" else "binarychoice(false, ?, $evaluatedCodec_false)"
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2019 PSForever
package net.psforever.newcodecs
import scodec._
import scodec.bits.BitVector
final class PrefixedVectorCodec[A](firstCodec: Codec[A], codec: Codec[A], limit: Option[Int] = None)
extends Codec[Vector[A]] {
def sizeBound =
limit match {
case None => SizeBound.unknown
case Some(lim) => codec.sizeBound * lim.toLong
}
def encode(vector: Vector[A]) =
Encoder.encodeSeq(firstCodec)(vector.slice(0, 1)).map { bits =>
if (vector.length > 1)
bits ++ (Encoder.encodeSeq(codec)(vector.tail) getOrElse BitVector.empty)
else
bits
}
def decode(buffer: BitVector): scodec.Attempt[scodec.DecodeResult[Vector[A]]] = {
Decoder.decodeCollect[Vector, A](firstCodec, Some(1))(buffer) match {
case Attempt.Successful(firstValue) =>
Decoder.decodeCollect[Vector, A](codec, limit map { _ - 1 })(firstValue.remainder) match {
case Attempt.Successful(secondValue) =>
Attempt.successful(DecodeResult(firstValue.value ++ secondValue.value, secondValue.remainder))
case Attempt.Failure(e) => Attempt.failure(e)
}
case Attempt.Failure(e) => Attempt.failure(e)
}
}
override def toString = s"vector($codec)"
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2017 PSForever
package net.psforever.newcodecs
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
import scodec.bits.{BitVector, ByteOrdering}
final class QuantizedDoubleCodec(min: Double, max: Double, bits: Int) extends Codec[Double] {
require(bits > 0 && bits <= 32, "bits must be in range [1, 32]")
private val bitsL = bits.toLong
private def description = s"$bits-bit q_double [$min, $max]"
override def sizeBound = SizeBound.exact(bitsL)
def QuantizeDouble(value: Double): Int = {
val range: Double = max - min;
if (range == 0.0)
return 0
val bit_max: Int = 1 << bits;
val rounded_quantized: Int = math.floor((value - min) * bit_max.toDouble / range + 0.5).toInt
if (rounded_quantized < 0)
return 0
if (rounded_quantized > bit_max - 1)
return (bit_max - 1)
return rounded_quantized
}
def UnquantizeDouble(value: Int): Double = {
return ((max - min) * value.toDouble / (1 << bitsL.toInt).toDouble + min)
}
override def encode(value: Double) = {
Attempt.successful(BitVector.fromInt(QuantizeDouble(value), bits, ByteOrdering.LittleEndian))
}
override def decode(buffer: BitVector) = {
if (buffer.sizeGreaterThanOrEqual(bitsL))
Attempt.successful(
DecodeResult(UnquantizeDouble(buffer.take(bitsL).toInt(false, ByteOrdering.LittleEndian)), buffer.drop(bitsL))
)
else
Attempt.failure(Err.insufficientBits(bitsL, buffer.size))
}
override def toString = description
}

View file

@ -0,0 +1,29 @@
// Copyright (c) 2017 PSForever
package net.psforever.newcodecs
import scodec.Attempt
import scodec._
package object newcodecs {
def q_double(min: Double, max: Double, bits: Int): Codec[Double] = new QuantizedDoubleCodec(min, max, bits)
def q_float(min: Double, max: Double, bits: Int): Codec[Float] =
q_double(min, max, bits).narrow(v => Attempt.successful(v.toFloat), _.toDouble)
def binary_choice[A](choice: Boolean, codec_true: => Codec[A], codec_false: => Codec[A]): Codec[A] =
new BinaryChoiceCodec(choice, codec_true, codec_false)
def prefixedVectorOfN[A](countCodec: Codec[Int], firstValueCodec: Codec[A], valueCodec: Codec[A]): Codec[Vector[A]] =
countCodec
.flatZip { count => new PrefixedVectorCodec(firstValueCodec, valueCodec, Some(count)) }
.narrow[Vector[A]](
{
case (cnt, xs) =>
if (xs.size == cnt) Attempt.successful(xs)
else
Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
},
xs => (xs.size, xs)
)
.withToString(s"vectorOfN($countCodec, $valueCodec)")
}

View file

@ -0,0 +1,7 @@
package net.psforever.objects
case class Account(
id: Int,
name: String,
gm: Boolean = false
)

View file

@ -0,0 +1,71 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.AmmoBoxDefinition
import net.psforever.objects.equipment.{Ammo, Equipment}
import net.psforever.types.PlanetSideEmpire
class AmmoBox(private val ammoDef: AmmoBoxDefinition, cap: Option[Int] = None) extends Equipment {
private var capacity = if (cap.isDefined) { AmmoBox.limitCapacity(cap.get, 1) }
else { FullCapacity }
def AmmoType: Ammo.Value = ammoDef.AmmoType
def Capacity: Int = capacity
def Capacity_=(toCapacity: Int): Int = {
capacity = AmmoBox.limitCapacity(toCapacity)
Capacity
}
def FullCapacity: Int = ammoDef.Capacity
def Definition: AmmoBoxDefinition = ammoDef
override def Faction_=(fact: PlanetSideEmpire.Value): PlanetSideEmpire.Value = Faction
override def toString: String = {
AmmoBox.toString(this)
}
}
object AmmoBox {
def apply(ammoDef: AmmoBoxDefinition): AmmoBox = {
new AmmoBox(ammoDef)
}
def apply(ammoDef: AmmoBoxDefinition, capacity: Int): AmmoBox = {
new AmmoBox(ammoDef, Some(capacity))
}
/**
* Accepting an `AmmoBox` object that has an uncertain amount of ammunition in it,
* create multiple `AmmoBox` objects where none contain more than the maximum capacity for that ammunition type,
* and the sum of all objects' capacities is the original object's capacity.
* The first element in the returned value is always the same object as the input object.
* Even if the original ammo object is not split, a list comprised of that same original object is returned.
* @param box an `AmmoBox` object of unspecified capacity
* @return a `List` of `AmmoBox` objects with correct capacities
*/
def Split(box: AmmoBox): List[AmmoBox] = {
val ammoDef = box.Definition
val boxCap: Int = box.Capacity
val maxCap: Int = ammoDef.Capacity
val splitCap: Int = boxCap / maxCap
box.Capacity = math.min(box.Capacity, maxCap)
val list: List[AmmoBox] = if (splitCap == 0) { Nil }
else { box +: List.fill(splitCap - 1)(new AmmoBox(ammoDef)) }
val leftover = boxCap - maxCap * splitCap
if (leftover > 0) {
list :+ AmmoBox(ammoDef, leftover)
} else {
list
}
}
def limitCapacity(count: Int, min: Int = 0): Int = math.min(math.max(min, count), 65535)
def toString(obj: AmmoBox): String = {
s"box of ${obj.AmmoType} ammo (${obj.Capacity})"
}
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
class BoomerDeployable(cdef: ExplosiveDeployableDefinition) extends ExplosiveDeployable(cdef) {
private var trigger: Option[BoomerTrigger] = None
def Trigger: Option[BoomerTrigger] = trigger
def Trigger_=(item: BoomerTrigger): Option[BoomerTrigger] = {
if (trigger.isEmpty) { //can only set trigger once
trigger = Some(item)
}
Trigger
}
def Trigger_=(item: Option[BoomerTrigger]): Option[BoomerTrigger] = {
if (item.isEmpty) {
trigger = None
}
Trigger
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.equipment.RemoteUnit
import net.psforever.types.PlanetSideEmpire
class BoomerTrigger extends SimpleItem(GlobalDefinitions.boomer_trigger) with RemoteUnit {
override def Faction_=(fact: PlanetSideEmpire.Value): PlanetSideEmpire.Value = Faction
}

View file

@ -0,0 +1,67 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.Certification
import net.psforever.objects.ce.DeployedItem
import net.psforever.objects.definition.{ConstructionFireMode, ConstructionItemDefinition}
import net.psforever.objects.equipment.{Equipment, FireModeSwitch}
/**
* A type of `Equipment` that can be wielded and applied to the game world to produce other game objects.<br>
* <br>
* Functionally, `ConstructionItem` objects resemble `Tool` objects that have fire mode state and alternate "ammunition."
* Very much unlike `Tool` object counterparts, however,
* the alternate "ammunition" is also a type of fire mode state
* maintained in a two-dimensional grid of related states.
* These states represent output products called deployables or, in the common vernacular, CE.
* Also unlike `Tool` objects, whose ammunition is always available even when drawing the weapon is not permitted,
* the different states are not all available if just the equipment itself is available.
* Parameters along with these CE states
* indicate whether the current output product is something the player is permitted to utilize.
* @param cItemDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
*/
class ConstructionItem(private val cItemDef: ConstructionItemDefinition)
extends Equipment
with FireModeSwitch[ConstructionFireMode] {
private var fireModeIndex: Int = 0
private var ammoTypeIndex: Int = 0
def FireModeIndex: Int = fireModeIndex
def FireModeIndex_=(index: Int): Int = {
fireModeIndex = index % Definition.Modes.length
FireModeIndex
}
def FireMode: ConstructionFireMode = Definition.Modes(fireModeIndex)
def NextFireMode: ConstructionFireMode = {
FireModeIndex = FireModeIndex + 1
ammoTypeIndex = 0
FireMode
}
def AmmoTypeIndex: Int = ammoTypeIndex
def AmmoTypeIndex_=(index: Int): Int = {
ammoTypeIndex = index % FireMode.Deployables.length
AmmoTypeIndex
}
def AmmoType: DeployedItem.Value = FireMode.Deployables(ammoTypeIndex)
def NextAmmoType: DeployedItem.Value = {
AmmoTypeIndex = AmmoTypeIndex + 1
FireMode.Deployables(ammoTypeIndex)
}
def ModePermissions: Set[Certification] = FireMode.Permissions(ammoTypeIndex)
def Definition: ConstructionItemDefinition = cItemDef
}
object ConstructionItem {
def apply(cItemDef: ConstructionItemDefinition): ConstructionItem = {
new ConstructionItem(cItemDef)
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
object Default {
//cancellable
import akka.actor.Cancellable
protected class InternalCancellable extends Cancellable {
override def cancel(): Boolean = true
override def isCancelled: Boolean = true
}
private val cancellable: Cancellable = new InternalCancellable
/**
* Used to initialize the value of a re-usable `Cancellable` object.
* By convention, it always acts like it has been cancelled before and can be cancelled.
* Should be replaced with pertinent `Cancellable` logic through the initialization of an executor.
*/
final def Cancellable: Cancellable = cancellable
//actor
import akka.actor.{Actor => AkkaActor, ActorRef, ActorSystem, DeadLetter, Props}
/**
* An actor designed to wrap around `deadLetters` and redirect all normal messages to it.
* This measure is more to "protect" `deadLetters` than anything else.
* Even if it is stopped, it still fulfills exactly the same purpose!
* The original target to which the actor is assigned will not be implicitly accredited.
*/
private class DefaultActor extends AkkaActor {
def receive: Receive = {
case msg => context.system.deadLetters ! DeadLetter(msg, sender(), self)
}
}
private var defaultRef: ActorRef = ActorRef.noSender
/**
* Instigate the default actor.
* @param sys the actor universe under which this default actor will exist
* @return the new default actor
*/
def apply(sys: ActorSystem): ActorRef = {
if (defaultRef == ActorRef.noSender) {
defaultRef = sys.actorOf(Props[DefaultActor](), name = s"system-default-actor")
}
defaultRef
}
final def Actor: ActorRef = defaultRef
}

View file

@ -0,0 +1,175 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.ActorRef
import net.psforever.objects.avatar.{Avatar, Certification}
import scala.concurrent.duration._
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{DeployableInfo, DeploymentAction}
import net.psforever.types.PlanetSideGUID
import net.psforever.services.RemoverActor
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
object Deployables {
private val log = org.log4s.getLogger("Deployables")
object Make {
def apply(item: DeployedItem.Value): () => PlanetSideGameObject with Deployable = cemap(item)
private val cemap: Map[DeployedItem.Value, () => PlanetSideGameObject with Deployable] = Map(
DeployedItem.boomer -> { () => new BoomerDeployable(GlobalDefinitions.boomer) },
DeployedItem.he_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.he_mine) },
DeployedItem.jammer_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.jammer_mine) },
DeployedItem.spitfire_turret -> { () => new TurretDeployable(GlobalDefinitions.spitfire_turret) },
DeployedItem.spitfire_cloaked -> { () => new TurretDeployable(GlobalDefinitions.spitfire_cloaked) },
DeployedItem.spitfire_aa -> { () => new TurretDeployable(GlobalDefinitions.spitfire_aa) },
DeployedItem.motionalarmsensor -> { () => new SensorDeployable(GlobalDefinitions.motionalarmsensor) },
DeployedItem.sensor_shield -> { () => new SensorDeployable(GlobalDefinitions.sensor_shield) },
DeployedItem.tank_traps -> { () => new TrapDeployable(GlobalDefinitions.tank_traps) },
DeployedItem.portable_manned_turret -> { () => new TurretDeployable(GlobalDefinitions.portable_manned_turret) },
DeployedItem.portable_manned_turret -> { () => new TurretDeployable(GlobalDefinitions.portable_manned_turret) },
DeployedItem.portable_manned_turret_nc -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_nc)
},
DeployedItem.portable_manned_turret_tr -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr)
},
DeployedItem.portable_manned_turret_vs -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs)
},
DeployedItem.deployable_shield_generator -> { () =>
new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator)
},
DeployedItem.router_telepad_deployable -> { () =>
new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
}
).withDefaultValue({ () => new ExplosiveDeployable(GlobalDefinitions.boomer) })
}
/**
* Distribute information that a deployable has been destroyed.
* The deployable may not have yet been eliminated from the game world (client or server),
* but its health is zero and it has entered the conditions where it is nearly irrelevant.<br>
* <br>
* The typical use case of this function involves destruction via weapon fire, attributed to a particular player.
* Contrast this to simply destroying a deployable by being the deployable's owner and using the map icon controls.
* This function eventually invokes the same routine
* but mainly goes into effect when the deployable has been destroyed
* and may still leave a physical component in the game world to be cleaned up later.
* That is the task `EliminateDeployable` performs.
* Additionally, since the player who destroyed the deployable isn't necessarily the owner,
* and the real owner will still be aware of the existence of the deployable,
* that player must be informed of the loss of the deployable directly.
* @see `DeployableRemover`
* @see `Vitality.DamageResolution`
* @see `LocalResponse.EliminateDeployable`
* @see `DeconstructDeployable`
* @param target the deployable that is destroyed
* @param time length of time that the deployable is allowed to exist in the game world;
* `None` indicates the normal un-owned existence time (180 seconds)
*/
def AnnounceDestroyDeployable(target: PlanetSideGameObject with Deployable, time: Option[FiniteDuration]): Unit = {
val zone = target.Zone
target.OwnerName match {
case Some(owner) =>
target.OwnerName = None
zone.LocalEvents ! LocalServiceMessage(owner, LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target))
case None => ;
}
zone.LocalEvents ! LocalServiceMessage(
s"${target.Faction}",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(target.GUID, Deployable.Icon(target.Definition.Item), target.Position, PlanetSideGUID(0))
)
)
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), zone))
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, zone, time))
}
/**
* Collect all deployables previously owned by the player,
* dissociate the avatar's globally unique identifier to remove turnover ownership,
* and, on top of performing the above manipulations, dispose of any boomers discovered.
* (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation
* if they had not already been handled by the time this function is executed.)
* @return all previously-owned deployables after they have been processed;
* boomers are listed before all other deployable types
*/
def Disown(zone: Zone, avatar: Avatar, replyTo: ActorRef): List[PlanetSideGameObject with Deployable] = {
val (boomers, deployables) =
avatar.deployables
.Clear()
.map(zone.GUID)
.collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] }
.partition(_.isInstanceOf[BoomerDeployable])
//do not change the OwnerName field at this time
boomers.collect({
case obj: BoomerDeployable =>
zone.LocalEvents.tell(
LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone, Some(0 seconds))),
replyTo
) //near-instant
obj.Owner = None
obj.Trigger = None
})
deployables.foreach(obj => {
zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone)), replyTo) //normal decay
obj.Owner = None
})
boomers ++ deployables
}
/**
* Initialize the deployables backend information.
* @param avatar the player's core
*/
def InitializeDeployableQuantities(avatar: Avatar): Boolean = {
log.info("Setting up combat engineering ...")
avatar.deployables.Initialize(avatar.certifications)
}
/**
* Initialize the UI elements for deployables.
* @param avatar the player's core
*/
def InitializeDeployableUIElements(avatar: Avatar): List[(Int, Int, Int, Int)] = {
log.info("Setting up combat engineering UI ...")
avatar.deployables.UpdateUI()
}
/**
* The player learned a new certification.
* Update the deployables user interface elements if it was an "Engineering" certification.
* The certification "Advanced Hacking" also relates to an element.
* @param certification the certification that was added
* @param certificationSet all applicable certifications
*/
def AddToDeployableQuantities(
avatar: Avatar,
certification: Certification,
certificationSet: Set[Certification]
): List[(Int, Int, Int, Int)] = {
avatar.deployables.AddToDeployableQuantities(certification, certificationSet)
avatar.deployables.UpdateUI(certification)
}
/**
* The player forgot a certification he previously knew.
* Update the deployables user interface elements if it was an "Engineering" certification.
* The certification "Advanced Hacking" also relates to an element.
* @param certification the certification that was added
* @param certificationSet all applicable certifications
*/
def RemoveFromDeployableQuantities(
avatar: Avatar,
certification: Certification,
certificationSet: Set[Certification]
): List[(Int, Int, Int, Int)] = {
avatar.deployables.RemoveFromDeployableQuantities(certification, certificationSet)
avatar.deployables.UpdateUI(certification)
}
}

View file

@ -0,0 +1,127 @@
// Copyright (c) 2018 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce._
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.vital.{StandardResolutions, Vitality}
import net.psforever.objects.zones.Zone
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.concurrent.duration._
class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) extends ComplexDeployable(cdef) with JammableUnit {
override def Definition: ExplosiveDeployableDefinition = cdef
}
class ExplosiveDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
Name = "explosive_deployable"
DeployCategory = DeployableCategory.Mines
Model = StandardResolutions.SimpleDeployables
Packet = new SmallDeployableConverter
private var detonateOnJamming: Boolean = true
def DetonateOnJamming: Boolean = detonateOnJamming
def DetonateOnJamming_=(detonate: Boolean): Boolean = {
detonateOnJamming = detonate
DetonateOnJamming
}
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[ExplosiveDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object ExplosiveDeployableDefinition {
def apply(dtype: DeployedItem.Value): ExplosiveDeployableDefinition = {
new ExplosiveDeployableDefinition(dtype.id)
}
}
class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with Damageable {
def DamageableObject = mine
def receive: Receive =
takesDamage
.orElse {
case _ => ;
}
protected def TakesDamage: Receive = {
case Vitality.Damage(applyDamageTo) =>
if (mine.CanDamage) {
val originalHealth = mine.Health
val cause = applyDamageTo(mine)
val damage = originalHealth - mine.Health
if (Damageable.CanDamageOrJammer(mine, damage, cause)) {
ExplosiveDeployableControl.DamageResolution(mine, cause, damage)
} else {
mine.Health = originalHealth
}
}
}
}
object ExplosiveDeployableControl {
def DamageResolution(target: ExplosiveDeployable, cause: ResolvedProjectile, damage: Int): Unit = {
target.History(cause)
if (target.Health == 0) {
DestructionAwareness(target, cause)
} else if (!target.Jammed && Damageable.CanJammer(target, cause)) {
if (
target.Jammed = {
val radius = cause.projectile.profile.DamageRadius
Vector3.DistanceSquared(cause.hit_pos, cause.target.Position) < radius * radius
}
) {
if (target.Definition.DetonateOnJamming) {
val zone = target.Zone
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target))
}
DestructionAwareness(target, cause)
}
}
}
/**
* na
* @param target na
* @param cause na
*/
def DestructionAwareness(target: ExplosiveDeployable, cause: ResolvedProjectile): Unit = {
val zone = target.Zone
val attribution = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match {
case Some(player) => player.GUID
case _ => PlanetSideGUID(0)
}
target.Destroyed = true
Deployables.AnnounceDestroyDeployable(target, Some(if (target.Jammed) 0 seconds else 500 milliseconds))
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.Destroy(target.GUID, attribution, Service.defaultPlayerGUID, target.Position)
)
if (target.Health == 0) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffect(Service.defaultPlayerGUID, "detonate_damaged_mine", target.GUID)
)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.KitDefinition
import net.psforever.objects.equipment.Equipment
/**
* A one-time-use recovery item that can be applied by the player while held within their inventory.
* @param kitDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
*/
class Kit(private val kitDef: KitDefinition) extends Equipment {
def Definition: KitDefinition = kitDef
}
object Kit {
def apply(kitDef: KitDefinition): Kit = {
new Kit(kitDef)
}
}

View file

@ -0,0 +1,105 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.Avatar
import scala.collection.concurrent.{Map, TrieMap}
/**
* See the companion object for class and method documentation.
* `LivePlayerList` is a singleton and this private class lacks exposure.
*/
private class LivePlayerList {
/** key - the session id; value - a `Player` object */
private val sessionMap: Map[Long, Avatar] = new TrieMap[Long, Avatar]
def WorldPopulation(predicate: ((_, Avatar)) => Boolean): List[Avatar] = {
sessionMap.filter(predicate).values.toList
}
def Add(sessionId: Long, avatar: Avatar): Boolean = {
sessionMap.values.find(char => char.equals(avatar)) match {
case None =>
sessionMap.putIfAbsent(sessionId, avatar).isEmpty
case Some(_) =>
false
}
}
def Update(sessionId: Long, avatar: Avatar): Unit = {
sessionMap.get(sessionId) match {
case Some(_) =>
sessionMap(sessionId) = avatar
case None => ;
}
}
def Remove(sessionId: Long): Option[Avatar] = {
sessionMap.remove(sessionId)
}
def Shutdown: List[Avatar] = {
val list = sessionMap.values.toList
sessionMap.clear()
list
}
}
/**
* A class for storing `Player` mappings for users that are currently online.
* The mapping system is tightly coupled between the `Avatar` class and to an instance of `WorldSessionActor`.
* <br>
* Use:<br>
* 1) When a users logs in during `WorldSessionActor`, associate that user's session id and their character (avatar).<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`LivePlayerList.Add(session, avatar)`<br>
* 2) In between the previous two steps, a range of characters may be queried based on provided statistics.<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`LivePlayerList.WorldPopulation(...)`<br>
* 3) When the user leaves the game entirely, his character's entry is removed from the mapping.<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`LivePlayerList.Remove(session)`
*/
object LivePlayerList {
/** As `LivePlayerList` is a singleton, an object of `LivePlayerList` is automatically instantiated. */
private val Instance: LivePlayerList = new LivePlayerList
/**
* Given some criteria, examine the mapping of user characters and find the ones that fulfill the requirements.<br>
* <br>
* Note the signature carefully.
* A two-element tuple is checked, but only the second element of that tuple - a `Player` - is eligible for being queried.
* The first element is ignored.
* Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason.
* @param predicate the conditions for filtering the live `Player`s
* @return a list of users's `Player`s that fit the criteria
*/
def WorldPopulation(predicate: ((_, Avatar)) => Boolean): List[Avatar] = Instance.WorldPopulation(predicate)
/**
* Create a mapped entry between the user's session and a user's character.
* Neither the player nor the session may exist in the current mappings if this is to work.
*
* @param sessionId the session
* @param avatar the character
* @return `true`, if the session was association was made; `false`, otherwise
*/
def Add(sessionId: Long, avatar: Avatar): Boolean = Instance.Add(sessionId, avatar)
def Update(sessionId: Long, avatar: Avatar): Unit = Instance.Update(sessionId, avatar)
/**
* Remove all entries related to the given session identifier from the mappings.
* The character no longer counts as "online."
*
* @param sessionId the session
* @return any character that was afffected by the mapping removal
*/
def Remove(sessionId: Long): Option[Avatar] = Instance.Remove(sessionId)
/**
* Hastily remove all mappings and ids.
*
* @return an unsorted list of the characters that were still online
*/
def Shutdown: List[Avatar] = Instance.Shutdown
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.ActorContext
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.PlanetSideEmpire
/**
* A `LocalProjectile` is a server-side object designed to populate a fake shared space.
* It is a placeholder intended to block out the existence of projectiles communicated from clients.
* All clients reserve the same internal range of user-generated GUID's from 40100 to 40124, inclusive.
* All clients recognize this same range independent of each other as "their own featureless projectiles."
* @see `Zone.MakeReservedObjects`<br>
* `Projectile.BaseUID`<br>
* `Projectile.RangeUID`
*/
class LocalProjectile extends PlanetSideServerObject {
def Faction = PlanetSideEmpire.NEUTRAL
def Definition = LocalProjectile.local
}
object LocalProjectile {
import net.psforever.objects.definition.ObjectDefinition
def local = new ObjectDefinition(0) { Name = "projectile" }
/**
* Instantiate and configure a `LocalProjectile` object.
* @param id the unique id that will be assigned to this entity
* @param context a context to allow the object to properly set up `ActorSystem` functionality
* @return the `LocalProjectile` object
*/
def Constructor(id: Int, context: ActorContext): LocalProjectile = {
new LocalProjectile()
}
}

View file

@ -0,0 +1,125 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.Actor
import net.psforever.objects.definition.EquipmentDefinition
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.packet.game.{ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectDetachMessage}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
/**
* The companion of a `Locker` that is carried with a player
* masquerading as their sixth `EquipmentSlot` object and a sub-inventory item.
* The `Player` class refers to it as the "fifth slot" as its permanent slot number is encoded as `0x85`.
* The inventory of this object is accessed using a game world `Locker` object (`mb_locker`).
*/
class LockerContainer extends PlanetSideServerObject with Container {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private val inventory = GridInventory(30, 20)
def Faction: PlanetSideEmpire.Value = faction
override def Faction_=(fact: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = fact
Faction
}
def Inventory: GridInventory = inventory
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = GlobalDefinitions.locker_container
}
object LockerContainer {
def apply(): LockerContainer = {
new LockerContainer()
}
}
class LockerEquipment(locker: LockerContainer) extends Equipment with Container {
private val obj = locker
override def GUID: PlanetSideGUID = obj.GUID
override def GUID_=(guid: PlanetSideGUID): PlanetSideGUID = obj.GUID_=(guid)
override def HasGUID: Boolean = obj.HasGUID
override def Invalidate(): Unit = obj.Invalidate()
override def Faction: PlanetSideEmpire.Value = obj.Faction
def Inventory: GridInventory = obj.Inventory
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = obj.Definition
}
class LockerContainerControl(locker: LockerContainer, toChannel: String) extends Actor with ContainableBehavior {
def ContainerObject = locker
def receive: Receive =
containerBehavior
.orElse {
case _ => ;
}
def MessageDeferredCallback(msg: Any): Unit = {
msg match {
case Containable.MoveItem(_, item, _) =>
//momentarily put item back where it was originally
val obj = ContainerObject
obj.Find(item) match {
case Some(slot) =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
toChannel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
)
case None => ;
}
case _ => ;
}
}
def RemoveItemFromSlotCallback(item: Equipment, slot: Int): Unit = {
val zone = locker.Zone
zone.AvatarEvents ! AvatarServiceMessage(toChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID))
}
def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = {
val zone = locker.Zone
val definition = item.Definition
item.Faction = PlanetSideEmpire.NEUTRAL
zone.AvatarEvents ! AvatarServiceMessage(
toChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(locker.GUID, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
)
}
def SwapItemCallback(item: Equipment, fromSlot: Int): Unit = {
val zone = locker.Zone
zone.AvatarEvents ! AvatarServiceMessage(
toChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectDetachMessage(locker.GUID, item.GUID, Vector3.Zero, 0f)
)
)
}
}

View file

@ -0,0 +1,90 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorRef}
import net.psforever.actors.commands.NtuCommand
import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer}
object Ntu {
object Nanites extends TransferContainer.TransferMaterial
/**
* Message for a `sender` announcing it has nanites it can offer the recipient.
*
* @param src the nanite container recognized as the sender
*/
final case class Offer(src: NtuContainer)
/**
* Message for a `sender` asking for nanites from the recipient.
*
* @param min a minimum amount of nanites requested;
* if 0, the `sender` has no expectations
* @param max the amount of nanites required to not make further requests;
* if 0, the `sender` is full and the message is for clean up operations
*/
final case class Request(min: Int, max: Int)
/**
* Message for transferring nanites to a recipient.
*
* @param src the nanite container recognized as the sender
* @param amount the nanites transferred in this package
*/
final case class Grant(src: NtuContainer, amount: Int)
}
trait NtuContainer extends TransferContainer {
def NtuCapacitor: Int
def NtuCapacitor_=(value: Int): Int
def Definition: NtuContainerDefinition
}
trait CommonNtuContainer extends NtuContainer {
private var ntuCapacitor: Int = 0
def NtuCapacitor: Int = ntuCapacitor
def NtuCapacitor_=(value: Int): Int = {
ntuCapacitor = scala.math.max(0, scala.math.min(value, Definition.MaxNtuCapacitor))
NtuCapacitor
}
def Definition: NtuContainerDefinition
}
trait NtuContainerDefinition {
private var maxNtuCapacitor: Int = 0
def MaxNtuCapacitor: Int = maxNtuCapacitor
def MaxNtuCapacitor_=(max: Int): Int = {
maxNtuCapacitor = max
MaxNtuCapacitor
}
}
trait NtuStorageBehavior extends Actor {
def NtuStorageObject: NtuContainer = null
def storageBehavior: Receive = {
case Ntu.Offer(src) => HandleNtuOffer(sender(), src)
case Ntu.Grant(_, 0) | Ntu.Request(0, 0) | TransferBehavior.Stopping() => StopNtuBehavior(sender())
case Ntu.Request(min, max) => HandleNtuRequest(sender(), min, max)
case Ntu.Grant(src, amount) => HandleNtuGrant(sender(), src, amount)
case NtuCommand.Grant(src, amount) => HandleNtuGrant(sender(), src, amount)
}
def HandleNtuOffer(sender: ActorRef, src: NtuContainer): Unit
def StopNtuBehavior(sender: ActorRef): Unit
def HandleNtuRequest(sender: ActorRef, min: Int, max: Int): Unit
def HandleNtuGrant(sender: ActorRef, src: NtuContainer, amount: Int): Unit
}

View 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"
}

View file

@ -0,0 +1,28 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
/**
* A size-checked unit of storage (or mounting) for `Equipment`.
* Unlike conventional `EquipmentSlot` space, this size of allowable `Equipment` is fixed.
* @param size the permanent size of the `Equipment` allowed in this slot
*/
class OffhandEquipmentSlot(size: EquipmentSize.Value) extends EquipmentSlot {
super.Size_=(size)
/**
* Not allowed to change the slot size manually.
* @param assignSize the changed in capacity for this slot
* @return the capacity for this slot
*/
override def Size_=(assignSize: EquipmentSize.Value): EquipmentSize.Value = Size
}
object OffhandEquipmentSlot {
/**
* An `EquipmentSlot` that can not be manipulated because its size is `Blocked` permanently.
*/
final val BlockedSlot = new OffhandEquipmentSlot(EquipmentSize.Blocked)
}

View file

@ -0,0 +1,65 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer {
private var owner: Option[PlanetSideGUID] = None
private var ownerName: Option[String] = None
def Owner: Option[PlanetSideGUID] = owner
def Owner_=(owner: PlanetSideGUID): Option[PlanetSideGUID] = Owner_=(Some(owner))
def Owner_=(owner: Player): Option[PlanetSideGUID] = Owner_=(Some(owner.GUID))
def Owner_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
owner match {
case Some(_) =>
this.owner = owner
case None =>
this.owner = None
}
Owner
}
def OwnerName: Option[String] = ownerName
def OwnerName_=(owner: String): Option[String] = OwnerName_=(Some(owner))
def OwnerName_=(owner: Player): Option[String] = OwnerName_=(Some(owner.Name))
def OwnerName_=(owner: Option[String]): Option[String] = {
owner match {
case Some(_) =>
ownerName = owner
case None =>
ownerName = None
}
OwnerName
}
/**
* na
* @param player na
* @return na
*/
def AssignOwnership(player: Player): OwnableByPlayer = AssignOwnership(Some(player))
/**
* na
* @param playerOpt na
* @return na
*/
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
playerOpt match {
case Some(player) =>
Owner = player
OwnerName = player
case None =>
Owner = None
OwnerName = None
}
this
}
}

View file

@ -0,0 +1,56 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.entity.{IdentifiableEntity, SimpleWorldEntity, WorldEntity}
import net.psforever.types.Vector3
/**
* A basic class that indicates an entity that exists somewhere in the world and has a globally unique identifier.
*/
abstract class PlanetSideGameObject extends IdentifiableEntity with WorldEntity {
private var entity: WorldEntity = new SimpleWorldEntity()
private var destroyed: Boolean = false
def Entity: WorldEntity = entity
def Entity_=(newEntity: WorldEntity): Unit = {
entity = newEntity
}
def Position: Vector3 = Entity.Position
def Position_=(vec: Vector3): Vector3 = {
Entity.Position = vec
}
def Orientation: Vector3 = Entity.Orientation
def Orientation_=(vec: Vector3): Vector3 = {
Entity.Orientation = vec
}
def Velocity: Option[Vector3] = Entity.Velocity
def Velocity_=(vec: Option[Vector3]): Option[Vector3] = {
Entity.Velocity = vec
}
def Destroyed: Boolean = destroyed
def Destroyed_=(state: Boolean): Boolean = {
destroyed = state
Destroyed
}
def Definition: ObjectDefinition
}
object PlanetSideGameObject {
def toString(obj: PlanetSideGameObject): String = {
val guid: String = if (obj.HasGUID) { obj.GUID.toString }
else { "NOGUID" }
val P = obj.Position
s"[$guid](x,y,z=${P.x % .3f},${P.y % .3f},${P.z % .3f})"
}
}

View file

@ -0,0 +1,537 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
import net.psforever.objects.zones.ZoneAware
import net.psforever.types.{PlanetSideGUID, _}
import scala.annotation.tailrec
import scala.util.{Success, Try}
class Player(var avatar: Avatar)
extends PlanetSideServerObject
with FactionAffinity
with Vitality
with ResistanceProfile
with Container
with JammableUnit
with ZoneAware {
Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead
Destroyed = true //see isAlive
private var backpack: Boolean = false
private var armor: Int = 0
private var capacitor: Float = 0f
private var capacitorState: CapacitorStateType.Value = CapacitorStateType.Idle
private var capacitorLastUsedMillis: Long = 0
private var capacitorLastChargedMillis: Long = 0
private var exosuit: ExoSuitDefinition = GlobalDefinitions.Standard
private val freeHand: EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory)
private val holsters: Array[EquipmentSlot] = Array.fill[EquipmentSlot](5)(new EquipmentSlot)
private val inventory: GridInventory = GridInventory()
private var drawnSlot: Int = Player.HandsDownSlot
private var lastDrawnSlot: Int = Player.HandsDownSlot
private var backpackAccess: Option[PlanetSideGUID] = None
private var facingYawUpper: Float = 0f
private var crouching: Boolean = false
private var jumping: Boolean = false
private var cloaked: Boolean = false
private var afk: Boolean = false
private var vehicleSeated: Option[PlanetSideGUID] = None
Continent = "home2" //the zone id
var spectator: Boolean = false
var silenced: Boolean = false
var death_by: Int = 0
var lastSeenStreamMessage: Array[Long] = Array.fill[Long](65535)(0L)
var lastShotSeq_time: Int = -1
/** From PlanetsideAttributeMessage */
var PlanetsideAttribute: Array[Long] = Array.ofDim(120)
val squadLoadouts = new LoadoutManager(10)
Player.SuitSetup(this, exosuit)
def Definition: AvatarDefinition = avatar.definition
def CharId: Long = avatar.id
def Name: String = avatar.name
def Faction: PlanetSideEmpire.Value = avatar.faction
def Sex: CharacterGender.Value = avatar.sex
def Head: Int = avatar.head
def Voice: CharacterVoice.Value = avatar.voice
def isAlive: Boolean = !Destroyed
def isBackpack: Boolean = backpack
def Spawn(): Boolean = {
if (!isAlive && !isBackpack) {
Destroyed = false
Health = Definition.DefaultHealth
Armor = MaxArmor
Capacitor = 0
}
isAlive
}
def Die: Boolean = {
Destroyed = true
Health = 0
false
}
def Revive: Boolean = {
Destroyed = false
Health = Definition.DefaultHealth
true
}
def Release: Boolean = {
if (!isAlive) {
backpack = true
true
} else {
false
}
}
def Armor: Int = armor
def Armor_=(assignArmor: Int): Int = {
armor = math.min(math.max(0, assignArmor), MaxArmor)
Armor
}
def MaxArmor: Int = exosuit.MaxArmor
def Capacitor: Float = capacitor
def Capacitor_=(value: Float): Float = {
val newValue = math.min(math.max(0, value), ExoSuitDef.MaxCapacitor.toFloat)
if (newValue < capacitor) {
capacitorLastUsedMillis = System.currentTimeMillis()
capacitorLastChargedMillis = 0
} else if (newValue > capacitor && newValue < ExoSuitDef.MaxCapacitor) {
capacitorLastChargedMillis = System.currentTimeMillis()
capacitorLastUsedMillis = 0
} else if (newValue > capacitor && newValue == ExoSuitDef.MaxCapacitor) {
capacitorLastChargedMillis = 0
capacitorLastUsedMillis = 0
capacitorState = CapacitorStateType.Idle
}
capacitor = newValue
capacitor
}
def CapacitorState: CapacitorStateType.Value = capacitorState
def CapacitorState_=(value: CapacitorStateType.Value): CapacitorStateType.Value = {
value match {
case CapacitorStateType.Charging => capacitorLastChargedMillis = System.currentTimeMillis()
case CapacitorStateType.Discharging => capacitorLastUsedMillis = System.currentTimeMillis()
case _ => ;
}
capacitorState = value
capacitorState
}
def CapacitorLastUsedMillis = capacitorLastUsedMillis
def CapacitorLastChargedMillis = capacitorLastChargedMillis
def VisibleSlots: Set[Int] =
if (exosuit.SuitType == ExoSuitType.MAX) {
Set(0)
} else {
(0 to 4).filterNot(index => holsters(index).Size == EquipmentSize.Blocked).toSet
}
override def Slot(slot: Int): EquipmentSlot = {
if (inventory.Offset <= slot && slot <= inventory.LastIndex) {
inventory.Slot(slot)
} else if (slot > -1 && slot < 5) {
holsters(slot)
} else if (slot == 5) {
avatar.fifthSlot()
} else if (slot == Player.FreeHandSlot) {
freeHand
} else {
OffhandEquipmentSlot.BlockedSlot
}
}
def Holsters(): Array[EquipmentSlot] = holsters
def Inventory: GridInventory = inventory
override def Fit(obj: Equipment): Option[Int] = {
recursiveHolsterFit(holsters.iterator, obj.Size) match {
case Some(index) =>
Some(index)
case None =>
inventory.Fit(obj.Definition.Tile) match {
case Some(index) =>
Some(index)
case None =>
if (freeHand.Equipment.isDefined) { None }
else { Some(Player.FreeHandSlot) }
}
}
}
@tailrec private def recursiveHolsterFit(
iter: Iterator[EquipmentSlot],
objSize: EquipmentSize.Value,
index: Int = 0
): Option[Int] = {
if (!iter.hasNext) {
None
} else {
val slot = iter.next()
if (slot.Equipment.isEmpty && slot.Size.equals(objSize)) {
Some(index)
} else {
recursiveHolsterFit(iter, objSize, index + 1)
}
}
}
def FreeHand = freeHand
def FreeHand_=(item: Option[Equipment]): Option[Equipment] = {
if (freeHand.Equipment.isEmpty || item.isEmpty) {
freeHand.Equipment = item
}
FreeHand.Equipment
}
override def Find(guid: PlanetSideGUID): Option[Int] = {
findInHolsters(holsters.iterator, guid)
.orElse(inventory.Find(guid)) match {
case Some(index) =>
Some(index)
case None =>
if (freeHand.Equipment.isDefined && freeHand.Equipment.get.GUID == guid) {
Some(Player.FreeHandSlot)
} else {
None
}
}
}
@tailrec private def findInHolsters(
iter: Iterator[EquipmentSlot],
guid: PlanetSideGUID,
index: Int = 0
): Option[Int] = {
if (!iter.hasNext) {
None
} else {
val slot = iter.next()
if (slot.Equipment.isDefined && slot.Equipment.get.GUID == guid) {
Some(index)
} else {
findInHolsters(iter, guid, index + 1)
}
}
}
override def Collisions(dest: Int, width: Int, height: Int): Try[List[InventoryItem]] = {
if (-1 < dest && dest < 5) {
holsters(dest).Equipment match {
case Some(item) =>
Success(List(InventoryItem(item, dest)))
case None =>
Success(List())
}
} else if (dest == Player.FreeHandSlot) {
freeHand.Equipment match {
case Some(item) =>
Success(List(InventoryItem(item, dest)))
case None =>
Success(List())
}
} else {
super.Collisions(dest, width, height)
}
}
def DrawnSlot: Int = drawnSlot
def DrawnSlot_=(slot: Int): Int = {
if (slot != drawnSlot) {
if (slot == Player.HandsDownSlot) {
drawnSlot = slot
} else if (VisibleSlots.contains(slot) && holsters(slot).Equipment.isDefined) {
drawnSlot = slot
lastDrawnSlot = slot
}
}
DrawnSlot
}
def LastDrawnSlot: Int = lastDrawnSlot
def ExoSuit: ExoSuitType.Value = exosuit.SuitType
def ExoSuitDef: ExoSuitDefinition = exosuit
def ExoSuit_=(suit: ExoSuitType.Value): Unit = {
val eSuit = ExoSuitDefinition.Select(suit, Faction)
exosuit = eSuit
Player.SuitSetup(this, eSuit)
ChangeSpecialAbility()
}
def Subtract = exosuit.Subtract
def ResistanceDirectHit = exosuit.ResistanceDirectHit
def ResistanceSplash = exosuit.ResistanceSplash
def ResistanceAggravated = exosuit.ResistanceAggravated
def RadiationShielding = exosuit.RadiationShielding
def FacingYawUpper: Float = facingYawUpper
def FacingYawUpper_=(facing: Float): Float = {
facingYawUpper = facing
FacingYawUpper
}
def Crouching: Boolean = crouching
def Crouching_=(crouched: Boolean): Boolean = {
crouching = crouched
Crouching
}
def Jumping: Boolean = jumping
def Jumping_=(jumped: Boolean): Boolean = {
jumping = jumped
Jumping
}
def Cloaked: Boolean = cloaked
def Cloaked_=(isCloaked: Boolean): Boolean = {
cloaked = isCloaked
Cloaked
}
def AwayFromKeyboard: Boolean = afk
def AwayFromKeyboard_=(away: Boolean): Boolean = {
afk = away
AwayFromKeyboard
}
private var usingSpecial: SpecialExoSuitDefinition.Mode.Value => SpecialExoSuitDefinition.Mode.Value =
DefaultUsingSpecial
private var gettingSpecial: () => SpecialExoSuitDefinition.Mode.Value = DefaultGettingSpecial
private def ChangeSpecialAbility(): Unit = {
if (ExoSuit == ExoSuitType.MAX) {
gettingSpecial = MAXGettingSpecial
usingSpecial = Faction match {
case PlanetSideEmpire.TR => UsingAnchorsOrOverdrive
case PlanetSideEmpire.NC => UsingShield
case _ => DefaultUsingSpecial
}
} else {
usingSpecial = DefaultUsingSpecial
gettingSpecial = DefaultGettingSpecial
}
}
def UsingSpecial: SpecialExoSuitDefinition.Mode.Value = { gettingSpecial() }
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
usingSpecial(state)
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
SpecialExoSuitDefinition.Mode.Normal
private def UsingAnchorsOrOverdrive(
state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial
val next = if (curr == Normal) {
if (state == Anchored || state == Overdrive) {
state
} else {
Normal
}
} else if (state == Normal) {
Normal
} else {
curr
}
MAXUsingSpecial(next)
}
private def UsingShield(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial
val next = if (curr == Normal) {
if (state == Shielded) {
state
} else {
Normal
}
} else if (state == Normal) {
Normal
} else {
curr
}
MAXUsingSpecial(next)
}
private def DefaultGettingSpecial(): SpecialExoSuitDefinition.Mode.Value = SpecialExoSuitDefinition.Mode.Normal
private def MAXUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
exosuit match {
case obj: SpecialExoSuitDefinition =>
obj.UsingSpecial = state
case _ =>
SpecialExoSuitDefinition.Mode.Normal
}
private def MAXGettingSpecial(): SpecialExoSuitDefinition.Mode.Value =
exosuit match {
case obj: SpecialExoSuitDefinition =>
obj.UsingSpecial
case _ =>
SpecialExoSuitDefinition.Mode.Normal
}
def isAnchored: Boolean =
ExoSuit == ExoSuitType.MAX && Faction == PlanetSideEmpire.TR && UsingSpecial == SpecialExoSuitDefinition.Mode.Anchored
def isOverdrived: Boolean =
ExoSuit == ExoSuitType.MAX && Faction == PlanetSideEmpire.TR && UsingSpecial == SpecialExoSuitDefinition.Mode.Overdrive
def isShielded: Boolean =
ExoSuit == ExoSuitType.MAX && Faction == PlanetSideEmpire.NC && UsingSpecial == SpecialExoSuitDefinition.Mode.Shielded
def AccessingBackpack: Option[PlanetSideGUID] = backpackAccess
def AccessingBackpack_=(guid: PlanetSideGUID): Option[PlanetSideGUID] = {
AccessingBackpack = Some(guid)
}
/**
* Change which player has access to the backpack of this player.
* A player may only access to the backpack of a dead released player, and only if no one else has access at the moment.
* @param guid the player who wishes to access the backpack
* @return the player who is currently allowed to access the backpack
*/
def AccessingBackpack_=(guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
guid match {
case None =>
backpackAccess = None
case Some(player) =>
if (isBackpack && backpackAccess.isEmpty) {
backpackAccess = Some(player)
}
}
AccessingBackpack
}
/**
* Can the other `player` access the contents of this `Player`'s backpack?
* @param player a player attempting to access this backpack
* @return `true`, if the `player` is permitted access; `false`, otherwise
*/
def CanAccessBackpack(player: Player): Boolean = {
isBackpack && (backpackAccess.isEmpty || backpackAccess.contains(player.GUID))
}
def VehicleSeated: Option[PlanetSideGUID] = vehicleSeated
def VehicleSeated_=(guid: PlanetSideGUID): Option[PlanetSideGUID] = VehicleSeated_=(Some(guid))
def VehicleSeated_=(guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
vehicleSeated = guid
VehicleSeated
}
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
override def equals(other: Any): Boolean =
other match {
case that: Player =>
(that canEqual this) &&
avatar == that.avatar
case _ =>
false
}
override def hashCode(): Int = {
avatar.hashCode()
}
override def toString: String = {
val guid = if (HasGUID) {
s" ${Continent}-${GUID.guid}"
} else {
""
}
s"${avatar.name}$guid ${Health}/${MaxHealth} ${Armor}/${MaxArmor}"
}
}
object Player {
final val LockerSlot: Int = 5
final val FreeHandSlot: Int = 250
final val HandsDownSlot: Int = 255
final case class Die()
def apply(core: Avatar): Player = {
new Player(core)
}
private def SuitSetup(player: Player, eSuit: ExoSuitDefinition): Unit = {
//inventory
player.Inventory.Clear()
player.Inventory.Resize(eSuit.InventoryScale.Width, eSuit.InventoryScale.Height)
player.Inventory.Offset = eSuit.InventoryOffset
//holsters
(0 until 5).foreach(index => { player.Slot(index).Size = eSuit.Holster(index) })
}
def Respawn(player: Player): Player = {
if (player.Release) {
val obj = new Player(player.avatar)
obj.Continent = player.Continent
obj
} else {
player
}
}
}

View file

@ -0,0 +1,137 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.packet.game.{InventoryStateMessage, RepairMessage}
import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.annotation.tailrec
object Players {
private val log = org.log4s.getLogger("Players")
/**
* Evaluate the progress of the user applying a tool to modify some other server object.
* This action is using the medical applicator to revive a fallen (dead but not released) ally.
*
* @param target the player being affected by the revive action
* @param user the player performing the revive action
* @param item the tool being used to revive the target player
* @param progress the current progress value
* @return `true`, if the next cycle of progress should occur;
* `false`, otherwise
*/
def RevivingTickAction(target: Player, user: Player, item: Tool)(progress: Float): Boolean = {
if (
!target.isAlive && !target.isBackpack &&
user.isAlive && !user.isMoving &&
user.Slot(user.DrawnSlot).Equipment.contains(item) && item.Magazine >= 25 &&
Vector3.Distance(target.Position, user.Position) < target.Definition.RepairDistance
) {
val events = target.Zone.AvatarEvents
val uname = user.Name
events ! AvatarServiceMessage(
uname,
AvatarAction.SendResponse(Service.defaultPlayerGUID, RepairMessage(target.GUID, progress.toInt))
)
true
} else {
false
}
}
/**
* na
* @see `AvatarAction.Revive`
* @see `AvatarResponse.Revive`
* @param target the player being revived
* @param medic the name of the player doing the reviving
* @param item the tool being used to revive the target player
*/
def FinishRevivingPlayer(target: Player, medic: String, item: Tool)(): Unit = {
val name = target.Name
log.info(s"$medic had revived $name")
val magazine = item.Discharge(Some(25))
target.Zone.AvatarEvents ! AvatarServiceMessage(
medic,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine)
)
)
target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID))
}
/**
* Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
* Remove any encountered items and add them to an output `List`.
* @param iter the `Iterator` of `EquipmentSlot`s
* @param index a number that equals the "current" holster slot (`EquipmentSlot`)
* @param list a persistent `List` of `Equipment` in the holster slots
* @return a `List` of `Equipment` in the holster slots
*/
@tailrec def clearHolsters(
iter: Iterator[EquipmentSlot],
index: Int = 0,
list: List[InventoryItem] = Nil
): List[InventoryItem] = {
if (!iter.hasNext) {
list
} else {
val slot = iter.next()
slot.Equipment match {
case Some(equipment) =>
slot.Equipment = None
clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list)
case None =>
clearHolsters(iter, index + 1, list)
}
}
}
/**
* Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
* For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot.
* Add that item to the slot and remove it from the list.
* @param iter the `Iterator` of `EquipmentSlot`s
* @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot
* @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot
*/
@tailrec def fillEmptyHolsters(iter: Iterator[EquipmentSlot], list: List[InventoryItem]): List[InventoryItem] = {
if (!iter.hasNext) {
list
} else {
val slot = iter.next()
if (slot.Equipment.isEmpty) {
list.find(item => item.obj.Size == slot.Size) match {
case Some(obj) =>
val index = list.indexOf(obj)
slot.Equipment = obj.obj
fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1))
case None =>
fillEmptyHolsters(iter, list)
}
} else {
fillEmptyHolsters(iter, list)
}
}
}
def CertificationToUseExoSuit(player: Player, exosuit: ExoSuitType.Value, subtype: Int): Boolean = {
ExoSuitDefinition.Select(exosuit, player.Faction).Permissions match {
case Nil =>
true
case permissions if subtype != 0 =>
val certs = player.avatar.certifications
certs.intersect(permissions.toSet).nonEmpty &&
certs.intersect(InfantryLoadout.DetermineSubtypeC(subtype)).nonEmpty
case permissions =>
player.avatar.certifications.intersect(permissions.toSet).nonEmpty
}
}
}

View file

@ -0,0 +1,151 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce._
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.StandardResolutions
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration._
class SensorDeployable(cdef: SensorDeployableDefinition) extends ComplexDeployable(cdef) with Hackable with JammableUnit
class SensorDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
Name = "sensor_deployable"
DeployCategory = DeployableCategory.Sensors
Model = StandardResolutions.SimpleDeployables
Packet = new SmallDeployableConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object SensorDeployableDefinition {
def apply(dtype: DeployedItem.Value): SensorDeployableDefinition = {
new SensorDeployableDefinition(dtype.id)
}
}
class SensorDeployableControl(sensor: SensorDeployable)
extends Actor
with JammableBehavior
with DamageableEntity
with RepairableEntity {
def JammableObject = sensor
def DamageableObject = sensor
def RepairableObject = sensor
def receive: Receive =
jammableBehavior
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
case _ => ;
}
override protected def DamageLog(msg: String): Unit = {}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
super.DestructionAwareness(target, cause)
SensorDeployableControl.DestructionAwareness(sensor, PlanetSideGUID(0))
}
override def StartJammeredSound(target: Any, dur: Int): Unit =
target match {
case obj: PlanetSideServerObject if !jammedSound =>
obj.Zone.VehicleEvents ! VehicleServiceMessage(
obj.Zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 54, 1)
)
super.StartJammeredSound(obj, dur)
case _ => ;
}
override def StartJammeredStatus(target: Any, dur: Int): Unit =
target match {
case obj: PlanetSideServerObject with JammableUnit if !obj.Jammed =>
val zone = obj.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, false, 1000)
)
super.StartJammeredStatus(obj, dur)
case _ => ;
}
override def CancelJammeredSound(target: Any): Unit = {
target match {
case obj: PlanetSideServerObject if jammedSound =>
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 54, 0)
)
case _ => ;
}
super.CancelJammeredSound(target)
}
override def CancelJammeredStatus(target: Any): Unit = {
target match {
case obj: PlanetSideServerObject with JammableUnit if obj.Jammed =>
sensor.Zone.LocalEvents ! LocalServiceMessage(
sensor.Zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, true, 1000)
)
case _ => ;
}
super.CancelJammeredStatus(target)
}
}
object SensorDeployableControl {
/**
* na
* @param target na
* @param attribution na
*/
def DestructionAwareness(target: Damageable.Target with Deployable, attribution: PlanetSideGUID): Unit = {
Deployables.AnnounceDestroyDeployable(target, Some(1 seconds))
val zone = target.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, false, 1000)
)
//position the explosion effect near the bulky area of the sensor stalk
val ang = target.Orientation
val explosionPos = {
val pos = target.Position
val yRadians = ang.y.toRadians
val d = Vector3.Rz(Vector3(0, 0.875f, 0), ang.z) * math.sin(yRadians).toFloat
Vector3(
pos.x + d.x,
pos.y + d.y,
pos.z + math.cos(yRadians).toFloat * 0.875f
)
}
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectLocation(Service.defaultPlayerGUID, "motion_sensor_destroyed", explosionPos, ang)
)
//TODO replaced by an alternate model (charred stub)?
}
}

View file

@ -0,0 +1,17 @@
package net.psforever.objects
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.zones.{Zone, Zoning}
import net.psforever.packet.game.DeadState
case class Session(
id: Long = 0,
zone: Zone = Zone.Nowhere,
account: Account = null,
player: Player = null,
avatar: Avatar = null,
zoningType: Zoning.Method.Value = Zoning.Method.None,
deadState: DeadState.Value = DeadState.Alive,
speed: Float = 1.0f,
flying: Boolean = false
)

View file

@ -0,0 +1,171 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCategory}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.types.PlanetSideGUID
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
extends ComplexDeployable(cdef)
with Hackable
with JammableUnit
class ShieldGeneratorDefinition extends ComplexDeployableDefinition(240) {
Packet = new ShieldGeneratorConverter
DeployCategory = DeployableCategory.ShieldGenerators
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
obj.Actor =
context.actorOf(Props(classOf[ShieldGeneratorControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
class ShieldGeneratorControl(gen: ShieldGeneratorDeployable)
extends Actor
with JammableBehavior
with DamageableEntity
with RepairableEntity {
def JammableObject = gen
def DamageableObject = gen
def RepairableObject = gen
private var handleDamageToShields: Boolean = false
def receive: Receive =
jammableBehavior
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
case _ => ;
}
/**
* The shield generator has two upgrade paths - blocking projectiles, and providing ammunition like a terminal.
* Both upgrade paths are possible using the nano dispenser with an armor canister,
* and can only be started when the generator is undamaged.
* @see `PlanetSideGameObject.CanRepair`
* @see `RepairableEntity.CanPerformRepairs`
* @param player the user of the nano dispenser tool
* @param item the nano dispenser tool
*/
override def CanBeRepairedByNanoDispenser(player: Player, item: Tool): Unit = {
if (gen.CanRepair) {
super.CanBeRepairedByNanoDispenser(player, item)
} else if (!gen.Destroyed) {
//TODO reinforced shield upgrade not implemented yet
//TODO ammunition supply upgrade not implemented yet
}
}
override protected def PerformDamage(
target: Damageable.Target,
applyDamageTo: ResolutionCalculations.Output
): Unit = {
val originalHealth = gen.Health
val originalShields = gen.Shields
val cause = applyDamageTo(target)
val health = gen.Health
val shields = gen.Shields
val damageToHealth = originalHealth - health
val damageToShields = originalShields - shields
val damage = damageToHealth + damageToShields
if (WillAffectTarget(target, damage, cause)) {
target.History(cause)
DamageLog(
target,
s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields"
)
handleDamageToShields = damageToShields > 0
HandleDamage(target, cause, damageToHealth)
} else {
gen.Health = originalHealth
gen.Shields = originalShields
}
}
override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Unit = {
super.DamageAwareness(target, cause, amount)
ShieldGeneratorControl.DamageAwareness(gen, cause, handleDamageToShields)
handleDamageToShields = false
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
super.DestructionAwareness(target, cause)
ShieldGeneratorControl.DestructionAwareness(gen, PlanetSideGUID(0))
}
/*
while the shield generator is technically a supported jammable target, how that works is currently unknown
check the object definition for proper feature activation
*/
override def StartJammeredSound(target: Any, dur: Int): Unit = {}
override def StartJammeredStatus(target: Any, dur: Int): Unit =
target match {
case obj: PlanetSideServerObject with JammableUnit if !obj.Jammed =>
obj.Zone.VehicleEvents ! VehicleServiceMessage(
obj.Zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 27, 1)
)
super.StartJammeredStatus(obj, dur)
case _ => ;
}
override def CancelJammeredSound(target: Any): Unit = {}
override def CancelJammeredStatus(target: Any): Unit = {
target match {
case obj: PlanetSideServerObject with JammableUnit if obj.Jammed =>
obj.Zone.VehicleEvents ! VehicleServiceMessage(
obj.Zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 27, 0)
)
case _ => ;
}
super.CancelJammeredStatus(target)
}
}
object ShieldGeneratorControl {
/**
* na
* @param target na
* @param cause na
* @param damageToShields na
*/
def DamageAwareness(target: ShieldGeneratorDeployable, cause: ResolvedProjectile, damageToShields: Boolean): Unit = {
//shields
if (damageToShields) {
val zone = target.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, target.Shields)
)
}
}
/**
* na
* @param target na
* @param attribution na
*/
def DestructionAwareness(target: Damageable.Target with Deployable, attribution: PlanetSideGUID): Unit = {
Deployables.AnnounceDestroyDeployable(target, None)
}
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.SimpleItemDefinition
import net.psforever.objects.equipment.Equipment
class SimpleItem(private val simpDef: SimpleItemDefinition) extends Equipment {
def Definition: SimpleItemDefinition = simpDef
}
object SimpleItem {
def apply(simpDef: SimpleItemDefinition): SimpleItem = {
new SimpleItem(simpDef)
}
}

View file

@ -0,0 +1,171 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
import net.psforever.objects.definition.{ObjectDefinition, VehicleDefinition}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.collection.mutable
trait SpawnPoint {
psso: PlanetSideServerObject =>
/**
* An element of the contract of `PlanetSideServerObject`;
* but, this makes it visible to a `SpawnPoint` object without casting.
* @see `Identifiable.GUID`
*/
def GUID: PlanetSideGUID
/**
* An element of the contract of `PlanetSideServerObject`;
* but, this makes it visible to a `SpawnPoint` object without casting.
* @see `WorldEntity.GUID`
* @see `SpecificPoint`
*/
def Position: Vector3
/**
* An element of the contract of `PlanetSideServerObject`;
* but, this makes it visible to a `SpawnPoint` object without casting.
* @see `WorldEntity.GUID`
* @see `SpecificPoint`
*/
def Orientation: Vector3
/**
* An element of an unspoken contract with `Amenity`.
* While not all `SpawnPoint` objects will be `Amenity` objects, a subclass of the `PlanetSideServerObject` class,
* they will all promote having an object owner, or "parent."
* This should generally be themselves.
* @see `Amenity.Owner`
*/
def Owner: PlanetSideServerObject
/**
* An element of the contract of `PlanetSideServerObject`;
* but, this makes it visible to a `SpawnPoint` object without casting.
* @see `PlanetSideGameObject.Definition`
* @see `SpecificPoint`
*/
def Definition: ObjectDefinition with SpawnPointDefinition
def Offline: Boolean = psso.Destroyed
/**
* Determine a specific position and orientation in which to spawn the target.
* @return a `Tuple` of `Vector3` objects;
* the first represents the game world position of spawning;
* the second represents the game world direction of spawning
*/
def SpecificPoint(target: PlanetSideGameObject): (Vector3, Vector3) = {
psso.Definition match {
case d: SpawnPointDefinition =>
d.SpecificPoint(this, target)
case _ =>
SpawnPoint.Default(this, target)
}
}
}
object SpawnPoint {
def Default(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = (obj.Position, obj.Orientation)
def Tube(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) =
(
obj.Position + Vector3.z(1.5f),
obj.Orientation.xy + Vector3.z(obj.Orientation.z + 90 % 360)
)
def AMS(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = {
//position the player alongside either of the AMS's terminals, facing away from it
val ori = obj.Orientation
val side = if (System.currentTimeMillis() % 2 == 0) 1 else -1 //right | left
val x = ori.x
val xsin = 3 * side * math.abs(math.sin(math.toRadians(x))).toFloat + 0.5f //sin because 0-degrees is up
val z = ori.z
val zrot = (z + 90) % 360
val zrad = math.toRadians(zrot)
val shift = Vector3(
math.sin(zrad).toFloat,
math.cos(zrad).toFloat,
0
) * (3 * side).toFloat //x=sin, y=cos because compass-0 is East, not North
(
obj.Position + shift + (if (x >= 330) { //ams leaning to the left
Vector3.z(xsin)
} else { //ams leaning to the right
Vector3.z(-xsin)
}),
if (side == 1) {
Vector3.z(zrot)
} else {
Vector3.z((z - 90) % 360)
}
)
}
def Gate(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = {
obj.Definition match {
case d: SpawnPointDefinition =>
val ori = target.Orientation
val zrad = math.toRadians(ori.z)
val radius =
scala.math.random().toFloat * d.UseRadius / 2 + 20f //20 is definitely outside of the gating energy field
val shift = Vector3(math.sin(zrad).toFloat, math.cos(zrad).toFloat, 0) * radius
val altitudeShift = target.Definition match {
case vdef: VehicleDefinition if GlobalDefinitions.isFlightVehicle(vdef) =>
Vector3.z(scala.math.random().toFloat * d.UseRadius / 4 + 20f)
case _ =>
Vector3.Zero
}
(obj.Position + shift + altitudeShift, ori)
case _ =>
Default(obj, target)
}
}
}
trait SpawnPointDefinition {
private var radius: Float = 0f //m
private var delay: Long = 0 //s
private var noWarp: Option[mutable.Set[VehicleDefinition]] = None
private var spawningFunc: (SpawnPoint, PlanetSideGameObject) => (Vector3, Vector3) = SpawnPoint.Default
def UseRadius: Float = radius
def UseRadius_=(rad: Float): Float = {
radius = rad
UseRadius
}
def Delay: Long = delay
def Delay_=(toDelay: Long): Long = {
delay = toDelay
Delay
}
def VehicleAllowance: Boolean = noWarp.isDefined
def VehicleAllowance_=(allow: Boolean): Boolean = {
if (allow && noWarp.isEmpty) {
noWarp = Some(mutable.Set.empty[VehicleDefinition])
} else if (!allow && noWarp.isDefined) {
noWarp = None
}
VehicleAllowance
}
def NoWarp: mutable.Set[VehicleDefinition] = {
noWarp.getOrElse(mutable.Set.empty[VehicleDefinition])
}
def SpecificPointFunc: (SpawnPoint, PlanetSideGameObject) => (Vector3, Vector3) = spawningFunc
def SpecificPointFunc_=(func: (SpawnPoint, PlanetSideGameObject) => (Vector3, Vector3)): Unit = {
spawningFunc = func
}
def SpecificPoint(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = spawningFunc(obj, target)
}

View file

@ -0,0 +1,13 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.definition.ConstructionItemDefinition
class Telepad(private val cdef: ConstructionItemDefinition) extends ConstructionItem(cdef) with TelepadLike
object Telepad {
def apply(cdef: ConstructionItemDefinition): Telepad = {
new Telepad(cdef)
}
}

View file

@ -0,0 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.ce.{SimpleDeployable, TelepadLike}
import net.psforever.objects.definition.SimpleDeployableDefinition
class TelepadDeployable(ddef: SimpleDeployableDefinition) extends SimpleDeployable(ddef) with TelepadLike

View file

@ -0,0 +1,241 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.{AmmoBoxDefinition, ProjectileDefinition, ToolDefinition}
import net.psforever.objects.equipment._
import net.psforever.objects.ballistics.Projectiles
import scala.annotation.tailrec
/**
* A type of `Equipment` that can be wielded and loaded with certain other game elements.<br>
* <br>
* "Tool" is a very mechanical name while this class is intended for various weapons and support items.
* The primary trait of a `Tool` is that it has something that counts as an "ammunition,"
* depleted as the `Tool` is used, replaceable as long as one has an appropriate type of `AmmoBox` object.
* (The former is always called "consuming;" the latter, "reloading.")
* Some weapons Chainblade have ammunition but do not consume it.
* @param toolDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
*/
class Tool(private val toolDef: ToolDefinition)
extends Equipment
with FireModeSwitch[FireModeDefinition]
with JammableUnit {
/** index of the current fire mode on the `ToolDefinition`'s list of fire modes */
private var fireModeIndex: Int = toolDef.DefaultFireModeIndex
/** current ammunition slot being used by this fire mode */
private var ammoSlots: List[Tool.FireModeSlot] = List.empty
var lastDischarge: Long = 0
Tool.LoadDefinition(this)
def FireModeIndex: Int = fireModeIndex
def FireModeIndex_=(index: Int): Int = {
fireModeIndex = index % Definition.FireModes.length
FireModeIndex
}
def FireMode: FireModeDefinition = Definition.FireModes(fireModeIndex)
def NextFireMode: FireModeDefinition = {
FireModeIndex = Definition.NextFireModeIndex(FireModeIndex)
AmmoSlot.Chamber = FireMode.Chamber
FireMode
}
def ToFireMode: Int = Definition.NextFireModeIndex(FireModeIndex)
def ToFireMode_=(index: Int): FireModeDefinition = {
FireModeIndex = index
AmmoSlot.Chamber = FireMode.Chamber
FireMode
}
def AmmoTypeIndex: Int = FireMode.AmmoTypeIndices(AmmoSlot.AmmoTypeIndex)
def AmmoTypeIndex_=(index: Int): Int = {
AmmoSlot.AmmoTypeIndex = index % FireMode.AmmoTypeIndices.length
AmmoTypeIndex
}
def AmmoType: Ammo.Value = Definition.AmmoTypes(AmmoTypeIndex).AmmoType
def NextAmmoType: Ammo.Value = {
AmmoSlot.AmmoTypeIndex = AmmoSlot.AmmoTypeIndex + 1
AmmoType
}
def Projectile: ProjectileDefinition = {
Definition.ProjectileTypes({
val projIndices = FireMode.ProjectileTypeIndices
if (projIndices.isEmpty) {
AmmoTypeIndex //e.g., bullet_9mm -> bullet_9mm_projectile, bullet_9mm_AP -> bullet_9mm_AP_projectile
} else {
projIndices(AmmoSlot.AmmoTypeIndex) //e.g., pulsar: f.mode1 -> pulsar_projectile, f.mode2 = pulsar_ap_projectile
}
})
}
def ProjectileType: Projectiles.Value = Projectile.ProjectileType
def Magazine: Int = AmmoSlot.Magazine
def Magazine_=(mag: Int): Int = {
//AmmoSlot.Magazine = Math.min(Math.max(0, mag), MaxMagazine)
AmmoSlot.Magazine = Math.max(0, mag)
Magazine
}
def MaxMagazine: Int = {
val fmode = FireMode
fmode.CustomMagazine.get(AmmoType) match {
case Some(magSize) =>
magSize
case None =>
fmode.Magazine
}
}
def Discharge(rounds: Option[Int] = None): Int = {
lastDischarge = System.nanoTime()
Magazine = FireMode.Discharge(this, rounds)
}
def LastDischarge: Long = {
lastDischarge
}
def AmmoSlot: Tool.FireModeSlot = ammoSlots(FireMode.AmmoSlotIndex)
def AmmoSlots: List[Tool.FireModeSlot] = ammoSlots
def MaxAmmoSlot: Int = ammoSlots.length
def Definition: ToolDefinition = toolDef
override def toString: String = Tool.toString(this)
}
//AmmoType = Definition.AmmoTypes( (Definition.FireModes(fireModeIndex)).AmmoTypeIndices( (ammoSlots((Definition.FireModes(fireModeIndex)).AmmoSlotIndex)).AmmoTypeIndex) ).AmmoType
object Tool {
def apply(toolDef: ToolDefinition): Tool = {
new Tool(toolDef)
}
/**
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
* @param tool the `Tool` being initialized
*/
def LoadDefinition(tool: Tool): Unit = {
val tdef: ToolDefinition = tool.Definition
val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex
tool.ammoSlots = buildFireModes(tdef, (0 to maxSlot).iterator, tdef.FireModes.toList)
}
@tailrec private def buildFireModes(
tdef: ToolDefinition,
iter: Iterator[Int],
fmodes: List[FireModeDefinition],
list: List[FireModeSlot] = Nil
): List[FireModeSlot] = {
if (!iter.hasNext) {
list
} else {
val index = iter.next()
fmodes.filter(fmode => fmode.AmmoSlotIndex == index) match {
case fmode :: _ =>
buildFireModes(tdef, iter, fmodes, list :+ new FireModeSlot(tdef, fmode))
case Nil =>
throw new IllegalArgumentException(
s"tool ${tdef.Name} ammo slot #$index is missing a fire mode specification; do not skip"
)
}
}
}
def toString(obj: Tool): String = {
s"${obj.Definition.Name} (mode=${obj.FireModeIndex}-${obj.AmmoType})(${obj.Magazine}/${obj.MaxMagazine})"
}
/**
* The `FireModeSlot` can be called the "magazine feed," an abstracted "ammunition slot."
* Most weapons will have only one ammunition slot and swap different ammunition into it as needed.
* In general to swap ammunition means to unload the onld ammunition and load the new ammunition.
* Many weapons also have one ammunition slot and multiple fire modes using the same list of ammunition
* This slot manages either of two ammunitions where one does not need to unload to be swapped to the other;
* however, the fire mod has most likely been changed.
* The Punisher -
* six ammunition types in total,
* two uniquely different types without unloading,
* two exclusive groups of ammunition divided into 2 cycled types and 4 cycled types -
* is an example of a weapon that benefits from this implementation.
*/
class FireModeSlot(private val tdef: ToolDefinition, private val fdef: FireModeDefinition) {
/**
* if this fire mode has multiple types of ammunition
* this is the index of the fire mode's ammo List, not a reference to the tool's ammo List
*/
private var ammoTypeIndex: Int = 0
/** a reference to the actual `AmmoBox` of this slot */
private var box: AmmoBox = AmmoBox(AmmoDefinition, fdef.Magazine)
private var chamber = fdef.Chamber
def AmmoTypeIndex: Int = ammoTypeIndex
def AmmoTypeIndex_=(index: Int): Int = {
ammoTypeIndex = index % fdef.AmmoTypeIndices.length
AmmoTypeIndex
}
private def AmmoDefinition: AmmoBoxDefinition = {
tdef.AmmoTypes(fdef.AmmoTypeIndices(ammoTypeIndex))
}
/**
* This is a reference to the `Ammo.Value` whose `AmmoBoxDefinition` should be loaded into `box`.
* It may not be the correct `Ammo.Value` whose `AmmoBoxDefinition` is loaded into `box` such as is the case during ammunition swaps.
* Generally, convert from this index, to the index in the fire mode's ammunition list, to the index in the `ToolDefinition`'s ammunition list.
* @return the `Ammo` type that should be loaded into the magazine right now
*/
def AmmoType: Ammo.Value = AmmoDefinition.AmmoType
def AllAmmoTypes: List[Ammo.Value] = {
fdef.AmmoTypeIndices.map(index => tdef.AmmoTypes(fdef.AmmoTypeIndices(index)).AmmoType).toList
}
def Magazine: Int = box.Capacity
def Magazine_=(mag: Int): Int = {
box.Capacity = mag
Magazine
}
def Chamber: Int = chamber
def Chamber_=(chmbr: Int): Int = {
chamber = math.min(math.max(0, chmbr), fdef.Chamber)
Chamber
}
def Box: AmmoBox = box
def Box_=(toBox: AmmoBox): Option[AmmoBox] = {
if (toBox.AmmoType == AmmoType) {
box = toBox
Some(Box)
} else {
None
}
}
def Tool: ToolDefinition = tdef
def Definition: FireModeDefinition = fdef
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.definition.converter.TRAPConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.StandardResolutions
class TrapDeployable(cdef: TrapDeployableDefinition) extends ComplexDeployable(cdef)
class TrapDeployableDefinition(objectId: Int) extends ComplexDeployableDefinition(objectId) {
Model = StandardResolutions.SimpleDeployables
Packet = new TRAPConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
obj.Actor = context.actorOf(Props(classOf[TrapDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object TrapDeployableDefinition {
def apply(dtype: DeployedItem.Value): TrapDeployableDefinition = {
new TrapDeployableDefinition(dtype.id)
}
}
class TrapDeployableControl(trap: TrapDeployable) extends Actor with DamageableEntity with RepairableEntity {
def DamageableObject = trap
def RepairableObject = trap
def receive: Receive =
takesDamage
.orElse(canBeRepairedByNanoDispenser)
.orElse {
case _ =>
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(trap, None)
}
}

View file

@ -0,0 +1,93 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.SmallTurretConverter
import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.MountableBehavior
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.{StandardResolutions, StandardVehicleResistance}
class TurretDeployable(tdef: TurretDeployableDefinition)
extends ComplexDeployable(tdef)
with WeaponTurret
with JammableUnit
with Hackable {
WeaponTurret.LoadDefinition(this)
def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap
override def Definition = tdef
}
class TurretDeployableDefinition(private val objectId: Int)
extends ComplexDeployableDefinition(objectId)
with TurretDefinition {
Name = "turret_deployable"
Packet = new SmallTurretConverter
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardVehicleResistance
Model = StandardResolutions.FacilityTurrets
//override to clarify inheritance conflict
override def MaxHealth: Int = super[ComplexDeployableDefinition].MaxHealth
//override to clarify inheritance conflict
override def MaxHealth_=(max: Int): Int = super[ComplexDeployableDefinition].MaxHealth_=(max)
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
override def Uninitialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
SimpleDeployableDefinition.SimpleUninitialize(obj, context)
}
}
object TurretDeployableDefinition {
def apply(dtype: DeployedItem.Value): TurretDeployableDefinition = {
new TurretDeployableDefinition(dtype.id)
}
}
/** control actors */
class TurretControl(turret: TurretDeployable)
extends Actor
with FactionAffinityBehavior.Check
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
with MountableBehavior.TurretMount
with MountableBehavior.Dismount
with DamageableWeaponTurret
with RepairableWeaponTurret {
def MountableObject = turret
def JammableObject = turret
def FactionObject = turret
def DamageableObject = turret
def RepairableObject = turret
def receive: Receive =
checkBehavior
.orElse(jammableBehavior)
.orElse(mountBehavior)
.orElse(dismountBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse {
case _ => ;
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(turret, None)
}
}

View file

@ -0,0 +1,698 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.annotation.tailrec
import scala.concurrent.duration.FiniteDuration
import scala.util.{Success, Try}
/**
* The server-side support object that represents a vehicle.<br>
* <br>
* All infantry seating, all mounted weapons, and the trunk space are considered part of the same index hierarchy.
* Generally, all seating is declared first - the driver and passengers and and gunners.
* Following that are the mounted weapons and other utilities.
* Trunk space starts being indexed afterwards.
* To keep it simple, infantry seating, mounted weapons, and utilities are stored separately herein.
* The `Map` of `Utility` objects is given using the same inventory index positions.
* Positive indices and zero are considered "represented" and must be assigned a globally unique identifier
* and must be present in the containing vehicle's `ObjectCreateMessage` packet.
* The index is the seat position, reflecting the position in the zero-index inventory.
* Negative indices are expected to be excluded from this conversion.
* The value of the negative index does not have a specific meaning.<br>
* <br>
* The importance of a vehicle's owner can not be overlooked.
* The owner is someone who can control who can sit in the vehicle's seats
* either through broad categorization or discriminating selection ("kicking")
* and who has access to and can allow access to the vehicle's trunk capacity.
* The driver is the only player that can access a vehicle's saved loadouts through a repair/rearm silo
* and can procure equipment from the said silo.
* The owner of a vehicle and the driver of a vehicle as mostly interchangeable terms for this reason
* and it can be summarized that the player who has access to the driver seat meets the qualifications for the "owner"
* so long as that player is the last person to have sat in that seat.
* All previous ownership information is replaced just as soon as someone else sits in the driver's seat.
* Ownership is also transferred as players die and respawn (from and to the same client)
* and when they leave a continent without taking the vehicle they currently own with them.
* (They also lose ownership when they leave the game, of course.)<br>
* <br>
* All seats have vehicle-level properties on top of their own internal properties.
* A seat has a glyph projected onto the ground when the vehicle is not moving
* that is used to mark where the seat can be accessed, as well as broadcasting the current access condition of the seat.
* As indicated previously, seats are composed into categories and the categories used to control access.
* The "driver" group has already been mentioned and is usually composed of a single seat, the "first" one.
* The driver seat is typically locked to the person who can sit in it - the owner - unless manually unlocked.
* Any seat besides the "driver" that has a weapon controlled from the seat is called a "gunner" seats.
* Any other seat besides the "driver" seat and "gunner" seats is called a "passenger" seat.
* All of these seats are typically unlocked normally.
* The "trunk" also counts as an access group even though it is not directly attached to a seat and starts as "locked."
* The categories all have their own glyphs,
* sharing a red cross glyph as a "can not access" state,
* and may also use their lack of visibility to express state.
* In terms of individual access, each seat can have its current occupant ejected, save for the driver's seat.
* @see `Vehicle.EquipmentUtilities`
* @param vehicleDef the vehicle's definition entry;
* stores and unloads pertinent information about the `Vehicle`'s configuration;
* used in the initialization process (`loadVehicleDefinition`)
*/
class Vehicle(private val vehicleDef: VehicleDefinition)
extends AmenityOwner
with Hackable
with FactionAffinity
with Mountable
with MountedWeapons
with Deployment
with Vitality
with OwnableByPlayer
with StandardResistanceProfile
with JammableUnit
with CommonNtuContainer
with Container {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None
private var jammered: Boolean = false
private var cloaked: Boolean = false
private var flying: Boolean = false
private var capacitor: Int = 0
/**
* Permissions control who gets to access different parts of the vehicle;
* the groups are Driver (seat), Gunner (seats), Passenger (seats), and the Trunk
*/
private val groupPermissions: Array[VehicleLockState.Value] =
Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked)
private var seats: Map[Int, Seat] = Map.empty
private var cargoHolds: Map[Int, Cargo] = Map.empty
private var weapons: Map[Int, EquipmentSlot] = Map.empty
private var utilities: Map[Int, Utility] = Map()
private val trunk: GridInventory = GridInventory()
/**
* Records the GUID of the cargo vehicle (galaxy/lodestar) this vehicle is stored in for DismountVehicleCargoMsg use
* DismountVehicleCargoMsg only passes the player_guid and this vehicle's guid
*/
private var mountedIn: Option[PlanetSideGUID] = None
private var vehicleGatingManifest: Option[VehicleManifest] = None
private var previousVehicleGatingManifest: Option[VehicleManifest] = None
//init
LoadDefinition()
/**
* Override this method to perform any special setup that is not standardized to `*Definition`.
* @see `Vehicle.LoadDefinition`
*/
protected def LoadDefinition(): Unit = {
Vehicle.LoadDefinition(this)
}
def Faction: PlanetSideEmpire.Value = {
this.faction
}
override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
this.faction = faction
faction
}
/** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */
def JackingDuration: Array[Int] = Definition.JackingDuration
def MountedIn: Option[PlanetSideGUID] = {
this.mountedIn
}
def MountedIn_=(cargo_vehicle_guid: PlanetSideGUID): Option[PlanetSideGUID] = MountedIn_=(Some(cargo_vehicle_guid))
def MountedIn_=(cargo_vehicle_guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
cargo_vehicle_guid match {
case Some(_) =>
this.mountedIn = cargo_vehicle_guid
case None =>
this.mountedIn = None
}
MountedIn
}
override def Health_=(assignHealth: Int): Int = {
//TODO should vehicle class enforce this?
if (!Destroyed) {
super.Health_=(assignHealth)
}
Health
}
def Shields: Int = {
shields
}
def Shields_=(strength: Int): Int = {
shields = math.min(math.max(0, strength), MaxShields)
Shields
}
def MaxShields: Int = {
Definition.MaxShields
}
def Decal: Int = {
decal
}
def Decal_=(logo: Int): Int = {
decal = logo
Decal
}
def Jammered: Boolean = jammered
def Jammered_=(jamState: Boolean): Boolean = {
jammered = jamState
Jammered
}
def Cloaked: Boolean = cloaked
def Cloaked_=(isCloaked: Boolean): Boolean = {
cloaked = isCloaked
Cloaked
}
def Flying: Boolean = flying
def Flying_=(isFlying: Boolean): Boolean = {
flying = isFlying
Flying
}
def NtuCapacitorScaled: Int = {
if (Definition.MaxNtuCapacitor > 0) {
scala.math.ceil((NtuCapacitor.toFloat / Definition.MaxNtuCapacitor.toFloat) * 10).toInt
} else {
0
}
}
def Capacitor: Int = capacitor
def Capacitor_=(value: Int): Int = {
if (value > Definition.MaxCapacitor) {
capacitor = Definition.MaxCapacitor
} else if (value < 0) {
capacitor = 0
} else {
capacitor = value
}
Capacitor
}
/**
* Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it.
* @param mountPoint an index representing the seat position / mounting point
* @return a seat number, or `None`
*/
def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = {
Definition.MountPoints.get(mountPoint)
}
def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap
/**
* What are the access permissions for a position on this vehicle, seats or trunk?
* @param group the group index
* @return what sort of access permission exist for this group
*/
def PermissionGroup(group: Int): Option[VehicleLockState.Value] = {
reindexPermissionsGroup(group) match {
case Some(index) =>
Some(groupPermissions(index))
case None =>
None
}
}
/**
* Change the access permissions for a position on this vehicle, seats or trunk.
* @param group the group index
* @param level the new permission for this group
* @return the new access permission for this group;
* `None`, if the group does not exist or the level of permission was not changed
*/
def PermissionGroup(group: Int, level: Long): Option[VehicleLockState.Value] = {
reindexPermissionsGroup(group) match {
case Some(index) =>
val current = groupPermissions(index)
val next =
try { VehicleLockState(level.toInt) }
catch { case _: Exception => groupPermissions(index) }
if (current != next) {
groupPermissions(index) = next
PermissionGroup(index)
} else {
None
}
case None =>
None
}
}
/**
* When the access permission group is communicated via `PlanetsideAttributeMessage`, the index is between 10 and 13.
* Internally, permission groups are stored as an `Array`, so the respective re-indexing plots 10 -> 0 and 13 -> 3.
* @param group the group index
* @return the modified group index
*/
private def reindexPermissionsGroup(group: Int): Option[Int] =
if (group > 9 && group < 14) {
Some(group - 10)
} else if (group > -1 && group < 4) {
Some(group)
} else {
None
}
/**
* Get the seat at the index.
* The specified "seat" can only accommodate a player as opposed to weapon mounts which share the same indexing system.
* @param seatNumber an index representing the seat position / mounting point
* @return a `Seat`, or `None`
*/
def Seat(seatNumber: Int): Option[Seat] = {
if (seatNumber >= 0 && seatNumber < this.seats.size) {
this.seats.get(seatNumber)
} else {
None
}
}
def Seats: Map[Int, Seat] = {
seats
}
def CargoHold(cargoNumber: Int): Option[Cargo] = {
if (cargoNumber >= 0) {
this.cargoHolds.get(cargoNumber)
} else {
None
}
}
def CargoHolds: Map[Int, Cargo] = {
cargoHolds
}
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
if (seatNumber == 0) {
Some(AccessPermissionGroup.Driver)
} else {
Seat(seatNumber) match {
case Some(seat) =>
seat.ControlledWeapon match {
case Some(_) =>
Some(AccessPermissionGroup.Gunner)
case None =>
Some(AccessPermissionGroup.Passenger)
}
case None =>
CargoHold(seatNumber) match {
case Some(_) =>
Some(AccessPermissionGroup.Passenger)
case None =>
None
}
}
}
}
def Weapons: Map[Int, EquipmentSlot] = weapons
/**
* Get the weapon at the index.
* @param wepNumber an index representing the seat position / mounting point
* @return a weapon, or `None`
*/
def ControlledWeapon(wepNumber: Int): Option[Equipment] = {
weapons.get(wepNumber) match {
case Some(mount) =>
mount.Equipment
case None =>
None
}
}
/**
* Given a player who may be an occupant, retrieve an number of the seat where this player is sat.
* @param player the player
* @return a seat number, or `None` if the `player` is not actually seated in this vehicle
*/
def PassengerInSeat(player: Player): Option[Int] = recursivePassengerInSeat(seats.iterator, player)
@tailrec private def recursivePassengerInSeat(iter: Iterator[(Int, Seat)], player: Player): Option[Int] = {
if (!iter.hasNext) {
None
} else {
val (seatNumber, seat) = iter.next()
if (seat.Occupant.contains(player)) {
Some(seatNumber)
} else {
recursivePassengerInSeat(iter, player)
}
}
}
def Utilities: Map[Int, Utility] = utilities
/**
* Get a reference to a certain `Utility` attached to this `Vehicle`.
* @param utilNumber the attachment number of the `Utility`
* @return the `Utility` or `None` (if invalid)
*/
def Utility(utilNumber: Int): Option[PlanetSideServerObject] = {
if (utilNumber >= 0 && utilNumber < this.utilities.size) {
this.utilities.get(utilNumber) match {
case Some(util) =>
Some(util())
case None =>
None
}
} else {
None
}
}
def Utility(utilType: UtilityType.Value): Option[PlanetSideServerObject] = {
utilities.values.find(_.UtilType == utilType) match {
case Some(util) =>
Some(util())
case None =>
None
}
}
override def DeployTime = Definition.DeployTime
override def UndeployTime = Definition.UndeployTime
def Inventory: GridInventory = trunk
def VisibleSlots: Set[Int] = weapons.keySet
override def Slot(slotNum: Int): EquipmentSlot = {
weapons
.get(slotNum)
// .orElse(utilities.get(slotNum) match {
// case Some(_) =>
// //TODO what do now?
// None
// case None => ;
// None
// })
.orElse(Some(Inventory.Slot(slotNum)))
.get
}
override def Find(guid: PlanetSideGUID): Option[Int] = {
weapons.find({
case (_, obj) =>
obj.Equipment match {
case Some(item) =>
if (item.HasGUID && item.GUID == guid) {
true
} else {
false
}
case None =>
false
}
}) match {
case Some((index, _)) =>
Some(index)
case None =>
Inventory.Find(guid)
}
}
override def Collisions(dest: Int, width: Int, height: Int): Try[List[InventoryItem]] = {
weapons.get(dest) match {
case Some(slot) =>
slot.Equipment match {
case Some(item) =>
Success(List(InventoryItem(item, dest)))
case None =>
Success(List())
}
case None =>
super.Collisions(dest, width, height)
}
}
/**
* A reference to the `Vehicle` `Trunk` space.
* @return this `Vehicle` `Trunk`
*/
def Trunk: GridInventory = {
this.trunk
}
def AccessingTrunk: Option[PlanetSideGUID] = trunkAccess
def AccessingTrunk_=(guid: PlanetSideGUID): Option[PlanetSideGUID] = {
AccessingTrunk = Some(guid)
}
/**
* Change which player has access to the trunk of this vehicle.
* A player may only gain access to the trunk if no one else has access to the trunk at the moment.
* @param guid the player who wishes to access the trunk
* @return the player who is currently allowed to access the trunk
*/
def AccessingTrunk_=(guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
guid match {
case None =>
trunkAccess = None
case Some(player) =>
if (trunkAccess.isEmpty) {
trunkAccess = Some(player)
}
}
AccessingTrunk
}
/**
* Can this `player` access the contents of this `Vehicle`'s `Trunk` given its current access permissions?
* @param player a player attempting to access this `Trunk`
* @return `true`, if the `player` is permitted access; `false`, otherwise
*/
def CanAccessTrunk(player: Player): Boolean = {
if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
groupPermissions(3) match {
case VehicleLockState.Locked => //only the owner
Owner.isEmpty || (Owner.isDefined && player.GUID == Owner.get)
case VehicleLockState.Group => //anyone in the owner's squad or platoon
faction == player.Faction //TODO this is not correct
case VehicleLockState.Empire => //anyone of the owner's faction
faction == player.Faction
}
} else {
false
}
}
/**
* Check access to the `Trunk`.
* @return the current access value for the `Vehicle` `Trunk`
*/
def TrunkLockState: VehicleLockState.Value = groupPermissions(3)
/**
* Trunk locations are stored as the orientation zero point being to the East. We need to convert that to a North = 0 orientation before returning the location
* @return A Vector3 of the current trunk location, orientated with North as the zero point
*/
def TrunkLocation: Vector3 = {
val rotationRadians = -math.toRadians(Orientation.z - 90f).toFloat
Vector3.PlanarRotateAroundPoint(Position + Definition.TrunkLocation, Position, rotationRadians)
}
def PrepareGatingManifest(): VehicleManifest = {
val manifest = VehicleManifest(this)
seats.collect { case (index, seat) if index > 0 => seat.Occupant = None }
vehicleGatingManifest = Some(manifest)
previousVehicleGatingManifest = None
manifest
}
def PublishGatingManifest(): Option[VehicleManifest] = {
val out = vehicleGatingManifest
previousVehicleGatingManifest = vehicleGatingManifest
vehicleGatingManifest = None
out
}
def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
/**
* This is the definition entry that is used to store and unload pertinent information about the `Vehicle`.
* @return the vehicle's definition entry
*/
def Definition: VehicleDefinition = vehicleDef
def canEqual(other: Any): Boolean = other.isInstanceOf[Vehicle]
override def equals(other: Any): Boolean =
other match {
case that: Vehicle =>
(that canEqual this) &&
hashCode() == that.hashCode()
case _ =>
false
}
override def hashCode(): Int = Actor.hashCode()
/**
* Override the string representation to provide additional information.
* @return the string output
*/
override def toString: String = {
Vehicle.toString(this)
}
}
object Vehicle {
/**
* A basic `Trait` connecting all of the actionable `Vehicle` 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 response the result of the processed request
*/
final case class VehicleMessages(player: Player, response: Exchange)
/**
* Initiate vehicle deconstruction.
* @see `VehicleControl`
* @param time the delay before deconstruction should initiate;
* should initiate instantly when `None`
*/
final case class Deconstruct(time: Option[FiniteDuration] = None)
/**
* The `Vehicle` will resume previous unresponsiveness to player activity.
* @see `VehicleControl`
*/
final case class Reactivate()
/**
* A request has been made to charge this vehicle's shields.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param amount the number of points to charge
*/
final case class ChargeShields(amount: Int)
/**
* Following a successful shield charge tick, display the results of the update.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param vehicle the updated vehicle
*/
final case class UpdateShieldsCharge(vehicle: Vehicle)
/**
* Change a vehicle's internal ownership property to match that of the target player.
* @param player the person who will own the vehicle, or `None` if the vehicle will go unowned
*/
final case class Ownership(player: Option[Player])
object Ownership {
def apply(player: Player): Ownership = Ownership(Some(player))
}
/**
* Overloaded constructor.
* @param vehicleDef the vehicle's definition entry
* @return a `Vehicle` object
*/
def apply(vehicleDef: VehicleDefinition): Vehicle = {
new Vehicle(vehicleDef)
}
/**
* Given a `Map` of `Utility` objects, only return the objects with a positive or zero-index position.
* @return a map of applicable utilities
*/
def EquipmentUtilities(utilities: Map[Int, Utility]): Map[Int, Utility] = {
utilities.filter({ case (index: Int, _: Utility) => index > -1 })
}
/**
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
* @param vehicle the `Vehicle` being initialized
* @see `{object}.LoadDefinition`
*/
def LoadDefinition(vehicle: Vehicle): Vehicle = {
val vdef: VehicleDefinition = vehicle.Definition
//general stuff
vehicle.Health = vdef.DefaultHealth
//create weapons
vehicle.weapons = vdef.Weapons
.map({
case (num, definition) =>
val slot = EquipmentSlot(EquipmentSize.VehicleWeapon)
slot.Equipment = Tool(definition)
num -> slot
})
.toMap
//create seats
vehicle.seats = vdef.Seats.map({ case (num, definition) => num -> Seat(definition) }).toMap
// create cargo holds
vehicle.cargoHolds = vdef.Cargo.map({ case (num, definition) => num -> Cargo(definition) }).toMap
//create utilities
vehicle.utilities = vdef.Utilities
.map({
case (num, util) =>
val obj = Utility(util, vehicle)
val utilObj = obj()
vehicle.Amenities = utilObj
utilObj.LocationOffset = vdef.UtilityOffset.get(num)
num -> obj
})
.toMap
//trunk
vdef.TrunkSize match {
case InventoryTile.None => ;
case dim =>
vehicle.trunk.Resize(dim.Width, dim.Height)
vehicle.trunk.Offset = vdef.TrunkOffset
}
vehicle
}
/**
* Provide a fixed string representation.
* @return the string output
*/
def toString(obj: Vehicle): String = {
val occupancy = obj.Seats.values.count(seat => seat.isOccupied)
s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
}
}

View file

@ -0,0 +1,411 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.transfer.TransferContainer
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.vehicles.{CargoBehavior, Utility, UtilityType, VehicleLockState}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.TriggeredSound
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration._
object Vehicles {
private val log = org.log4s.getLogger("Vehicles")
/**
* na
*
* @param vehicle na
* @param player na
* @return na
*/
def Own(vehicle: Vehicle, player: Player): Option[Vehicle] = Own(vehicle, Some(player))
/**
* na
* @param vehicle na
* @param playerOpt na
* @return na
*/
def Own(vehicle: Vehicle, playerOpt: Option[Player]): Option[Vehicle] = {
playerOpt match {
case Some(tplayer) =>
tplayer.avatar.vehicle = Some(vehicle.GUID)
vehicle.AssignOwnership(playerOpt)
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
vehicle.Zone.id,
VehicleAction.Ownership(tplayer.GUID, vehicle.GUID)
)
Vehicles.ReloadAccessPermissions(vehicle, tplayer.Name)
Some(vehicle)
case None =>
None
}
}
/**
* Disassociate a vehicle from the player who owns it.
*
* @param guid the unique identifier for that vehicle
* @param vehicle the vehicle
* @return the vehicle, if it had a previous owner;
* `None`, otherwise
*/
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
vehicle.Zone.GUID(vehicle.Owner) match {
case Some(player: Player) =>
if (player.avatar.vehicle.contains(guid)) {
player.avatar.vehicle = None
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
player.Name,
VehicleAction.Ownership(player.GUID, PlanetSideGUID(0))
)
}
vehicle.AssignOwnership(None)
val empire = VehicleLockState.Empire.id
val factionChannel = s"${vehicle.Faction}"
(0 to 2).foreach(group => {
vehicle.PermissionGroup(group, empire)
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
factionChannel,
VehicleAction.SeatPermissions(Service.defaultPlayerGUID, guid, group, empire)
)
})
ReloadAccessPermissions(vehicle, player.Name)
Some(vehicle)
case _ =>
None
}
/**
* Disassociate a player from a vehicle that he owns.
* The vehicle must exist in the game world on the specified continent.
* This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat.
* This is the player side of vehicle ownership removal.
* @param player the player
*/
def Disown(player: Player, zone: Zone): Option[Vehicle] = Disown(player, Some(zone))
/**
* Disassociate a player from a vehicle that he owns.
* The vehicle must exist in the game world on the specified continent.
* This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat.
* This is the player side of vehicle ownership removal.
* @param player the player
*/
def Disown(player: Player, zoneOpt: Option[Zone]): Option[Vehicle] = {
player.avatar.vehicle match {
case Some(vehicle_guid) =>
player.avatar.vehicle = None
zoneOpt.getOrElse(player.Zone).GUID(vehicle_guid) match {
case Some(vehicle: Vehicle) =>
Disown(player, vehicle)
case _ =>
None
}
case None =>
None
}
}
/**
* Disassociate a player from a vehicle that he owns without associating a different player as the owner.
* Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire,"
* then reload them for all clients.
* This is the vehicle side of vehicle ownership removal.
* @param player the player
*/
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
val pguid = player.GUID
if (vehicle.Owner.contains(pguid)) {
vehicle.AssignOwnership(None)
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID
val empire = VehicleLockState.Empire.id
(0 to 2).foreach(group => {
vehicle.PermissionGroup(group, empire)
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
s"${vehicle.Faction}",
VehicleAction.SeatPermissions(pguid, vguid, group, empire)
)
})
ReloadAccessPermissions(vehicle, player.Name)
Some(vehicle)
} else {
None
}
}
/**
* Iterate over vehicle permissions and turn them into `PlanetsideAttributeMessage` packets.<br>
* <br>
* For the purposes of ensuring that other players are always aware of the proper permission state of the trunk and seats,
* packets are intentionally dispatched to the current client to update the states.
* Perform this action just after any instance where the client would initially gain awareness of the vehicle.
* The most important examples include either the player or the vehicle itself spawning in for the first time.
* @param vehicle the `Vehicle`
*/
def ReloadAccessPermissions(vehicle: Vehicle, toChannel: String): Unit = {
val vehicle_guid = vehicle.GUID
(0 to 3).foreach(group => {
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
toChannel,
AvatarAction.PlanetsideAttributeToAll(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id)
)
})
}
/**
* A recursive test that explores all the seats of a target vehicle
* and all the seats of any discovered cargo vehicles
* and then the same criteria in those cargo vehicles
* to determine if any of their combined passenger roster remains in a given zone.<br>
* <br>
* The original zone is expected to be defined in the internal vehicle gating manifest file
* and, if this file does not exist, we fail the testing process.
* The target zone is the one wherever the vehicle currently is located (`vehicle.Zone`).
* All participant passengers, also defined in the manifest, are expected to be in the target zone at the same time.
* This test excludes (rejects) same-zone transitions
* though it would automatically pass the test under those conditions.<br>
* <br>
* While it should be possible to recursively explore up a parent-child relationship -
* testing the ferrying vehicle to which the current tested vehicle is considered a cargo vehicle -
* the relationship expressed is one of globally unique refertences and not one of object references -
* that suggested super-ferrying vehicle may not exist in the zone unless special considerations are imposed.
* For the purpose of these special considerations,
* implemented by enforcing a strictly downwards order of vehicular zone transportation,
* where drivers move vehicles and call passengers and immediate cargo vehicle drivers,
* it becomes unnecessary to test any vehicle that might be ferrying the target vehicle.
* @see `ZoneAware`
* @param vehicle the target vehicle being moved around between zones
* @return `true`, if all passengers of the vehicle, and its cargo vehicles, etc., have reported being in the same zone;
* `false`, if no manifest entry exists, or if the vehicle is moving to the same zone
*/
def AllGatedOccupantsInSameZone(vehicle: Vehicle): Boolean = {
val vzone = vehicle.Zone
vehicle.PreviousGatingManifest() match {
case Some(manifest) if vzone != manifest.origin =>
val manifestPassengers = manifest.passengers.collect { case (name, _) => name } :+ manifest.driverName
val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) }
manifestPassengerResults.forall(_ == true) &&
vehicle.CargoHolds.values
.collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.Occupant.get) }
.forall(_ == true)
case _ =>
false
}
}
/**
* The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle.
* The type of carrier is not an important consideration in determining the orientation, oddly enough.
* @param vehicle the cargo vehicle
* @return the orientation as an `Integer` value;
* `0` for almost all cases
*/
def CargoOrientation(vehicle: Vehicle): Int = {
if (vehicle.Definition == GlobalDefinitions.router) {
1
} else {
0
}
}
/**
* The process of hacking/jacking a vehicle is complete.
* Change the faction of the vehicle to the hacker's faction and remove all occupants.
* @param target The `Vehicle` object that has been hacked/jacked
* @param hacker the one whoi performed the hack and will inherit ownership of the target vehicle
* @param unk na; used by `HackMessage` as `unk5`
*/
def FinishHackingVehicle(target: Vehicle, hacker: Player, unk: Long)(): Unit = {
log.info(s"Vehicle guid: ${target.GUID} has been jacked")
val zone = target.Zone
// Forcefully dismount any cargo
target.CargoHolds.values.foreach(cargoHold => {
cargoHold.Occupant match {
case Some(cargo: Vehicle) => {
cargo.Seats(0).Occupant match {
case Some(cargoDriver: Player) =>
CargoBehavior.HandleVehicleCargoDismount(
target.Zone,
cargo.GUID,
bailed = target.Flying,
requestedByPassenger = false,
kicked = true
)
case None =>
log.error("FinishHackingVehicle: vehicle in cargo hold missing driver")
CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, false, false, true)
}
}
case None => ;
}
})
// Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => {
seat.Occupant match {
case Some(tplayer) =>
seat.Occupant = None
tplayer.VehicleSeated = None
if (tplayer.HasGUID) {
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)
)
}
case None => ;
}
})
// If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed.
if (target.Definition.CanFly && target.Flying) {
// todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board?
target.Actor ! Vehicle.Deconstruct()
} else { // Otherwise handle ownership transfer as normal
// Remove ownership of our current vehicle, if we have one
hacker.avatar.vehicle match {
case Some(guid: PlanetSideGUID) =>
zone.GUID(guid) match {
case Some(vehicle: Vehicle) =>
Vehicles.Disown(hacker, vehicle)
case _ => ;
}
case _ => ;
}
target.Owner match {
case Some(previousOwnerGuid: PlanetSideGUID) =>
// Remove ownership of the vehicle from the previous player
zone.GUID(previousOwnerGuid) match {
case Some(tplayer: Player) =>
Vehicles.Disown(tplayer, target)
case _ => ; // Vehicle already has no owner
}
case _ => ;
}
// Now take ownership of the jacked vehicle
target.Actor ! CommonMessages.Hack(hacker, target)
target.Faction = hacker.Faction
Vehicles.Own(target, hacker)
//todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary.
// And broadcast the faction change to other clients
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, target.GUID, hacker.Faction)
)
}
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerSound(hacker.GUID, TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f)
)
// Clean up after specific vehicles, e.g. remove router telepads
// If AMS is deployed, swap it to the new faction
target.Definition match {
case GlobalDefinitions.router =>
Vehicles.RemoveTelepads(target)
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
case _ => ;
}
}
def FindANTChargingSource(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
//determine if we are close enough to charge from something
(ntuChargingTarget match {
case Some(target: WarpGate) if {
val soiRadius = target.Definition.SOIRadius
Vector3.DistanceSquared(obj.Position.xy, target.Position.xy) < soiRadius * soiRadius
} =>
Some(target.asInstanceOf[NtuContainer])
case None =>
None
}).orElse {
val position = obj.Position.xy
obj.Zone.Buildings.values
.collectFirst {
case gate: WarpGate if {
val soiRadius = gate.Definition.SOIRadius
Vector3.DistanceSquared(position, gate.Position.xy) < soiRadius * soiRadius
} =>
gate
}
.asInstanceOf[Option[NtuContainer]]
}
}
def FindANTDischargingTarget(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
(ntuChargingTarget match {
case out @ Some(target: NtuContainer) if {
Vector3.DistanceSquared(obj.Position.xy, target.Position.xy) < 400 //20m is generous ...
} =>
out
case _ =>
None
}).orElse {
val position = obj.Position.xy
obj.Zone.Buildings.values
.find { building =>
building.BuildingType == StructureType.Facility && {
val soiRadius = building.Definition.SOIRadius
Vector3.DistanceSquared(position, building.Position.xy) < soiRadius * soiRadius
}
} match {
case Some(building) =>
building.Amenities
.collect { case obj: NtuContainer => obj }
.sortBy { o => Vector3.DistanceSquared(position, o.Position.xy) < 400 } //20m is generous ...
.headOption
case None =>
None
}
}
}
/**
* Before a vehicle is removed from the game world, the following actions must be performed.
*
* @param vehicle the vehicle
*/
def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = {
vehicle.Definition match {
case GlobalDefinitions.ams =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case GlobalDefinitions.ant =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case GlobalDefinitions.router =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case _ => ;
}
}
def RemoveTelepads(vehicle: Vehicle): Unit = {
val zone = vehicle.Zone
(vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) =>
val telepad = util.Telepad
util.Telepad = None
zone.GUID(telepad)
case _ =>
None
}) match {
case Some(telepad: TelepadDeployable) =>
log.debug(s"BeforeUnload: deconstructing telepad $telepad that was linked to router $vehicle ...")
telepad.Active = false
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), zone))
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, zone, Some(0 seconds)))
case _ => ;
}
}
}

View file

@ -0,0 +1,188 @@
package net.psforever.objects.avatar
import net.psforever.objects.definition.{AvatarDefinition, BasicDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
import net.psforever.objects.loadouts.{Loadout, SquadLoadout}
import net.psforever.objects.{GlobalDefinitions, LockerContainer, LockerEquipment, OffhandEquipmentSlot}
import net.psforever.types._
import org.joda.time.{LocalDateTime, Period}
import scala.collection.immutable.Seq
import scala.concurrent.duration.{FiniteDuration, _}
object Avatar {
val purchaseCooldowns: Map[BasicDefinition, FiniteDuration] = Map(
GlobalDefinitions.ams -> 5.minutes,
GlobalDefinitions.ant -> 5.minutes,
GlobalDefinitions.apc_nc -> 5.minutes,
GlobalDefinitions.apc_tr -> 5.minutes,
GlobalDefinitions.apc_vs -> 5.minutes,
GlobalDefinitions.aurora -> 5.minutes,
GlobalDefinitions.battlewagon -> 5.minutes,
GlobalDefinitions.dropship -> 5.minutes,
GlobalDefinitions.flail -> 5.minutes,
GlobalDefinitions.fury -> 5.minutes,
GlobalDefinitions.galaxy_gunship -> 10.minutes,
GlobalDefinitions.lodestar -> 5.minutes,
GlobalDefinitions.liberator -> 5.minutes,
GlobalDefinitions.lightgunship -> 5.minutes,
GlobalDefinitions.lightning -> 5.minutes,
GlobalDefinitions.magrider -> 5.minutes,
GlobalDefinitions.mediumtransport -> 5.minutes,
GlobalDefinitions.mosquito -> 5.minutes,
GlobalDefinitions.phantasm -> 5.minutes,
GlobalDefinitions.prowler -> 5.minutes,
GlobalDefinitions.quadassault -> 5.minutes,
GlobalDefinitions.quadstealth -> 5.minutes,
GlobalDefinitions.router -> 5.minutes,
GlobalDefinitions.switchblade -> 5.minutes,
GlobalDefinitions.skyguard -> 5.minutes,
GlobalDefinitions.threemanheavybuggy -> 5.minutes,
GlobalDefinitions.thunderer -> 5.minutes,
GlobalDefinitions.two_man_assault_buggy -> 5.minutes,
GlobalDefinitions.twomanhoverbuggy -> 5.minutes,
GlobalDefinitions.twomanheavybuggy -> 5.minutes,
GlobalDefinitions.vanguard -> 5.minutes,
GlobalDefinitions.vulture -> 5.minutes,
GlobalDefinitions.wasp -> 5.minutes,
GlobalDefinitions.flamethrower -> 3.minutes,
GlobalDefinitions.VSMAX -> 5.minutes,
GlobalDefinitions.NCMAX -> 5.minutes,
GlobalDefinitions.TRMAX -> 5.minutes,
// TODO weapon based cooldown
GlobalDefinitions.nchev_sparrow -> 5.minutes,
GlobalDefinitions.nchev_falcon -> 5.minutes,
GlobalDefinitions.nchev_scattercannon -> 5.minutes,
GlobalDefinitions.vshev_comet -> 5.minutes,
GlobalDefinitions.vshev_quasar -> 5.minutes,
GlobalDefinitions.vshev_starfire -> 5.minutes,
GlobalDefinitions.trhev_burster -> 5.minutes,
GlobalDefinitions.trhev_dualcycler -> 5.minutes,
GlobalDefinitions.trhev_pounder -> 5.minutes
)
val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map(
GlobalDefinitions.medkit -> 5.seconds,
GlobalDefinitions.super_armorkit -> 20.minutes,
GlobalDefinitions.super_medkit -> 20.minutes,
GlobalDefinitions.super_staminakit -> 20.minutes
)
}
case class Avatar(
/** unique identifier corresponding to a database table row index */
id: Int,
name: String,
faction: PlanetSideEmpire.Value,
sex: CharacterGender.Value,
head: Int,
voice: CharacterVoice.Value,
bep: Long = 0,
cep: Long = 0,
stamina: Int = 100,
fatigued: Boolean = false,
cosmetics: Option[Set[Cosmetic]] = None,
certifications: Set[Certification] = Set(),
loadouts: Seq[Option[Loadout]] = Seq.fill(15)(None),
squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None),
implants: Seq[Option[Implant]] = Seq(None, None, None),
locker: LockerContainer = new LockerContainer(), // TODO var bad
deployables: DeployableToolbox = new DeployableToolbox(), // TODO var bad
lookingForSquad: Boolean = false,
var vehicle: Option[PlanetSideGUID] = None, // TODO var bad
firstTimeEvents: Set[String] =
FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++
FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++
FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++
FirstTimeEvents.Generic,
/** Timestamps of when a vehicle or equipment was last purchased */
purchaseTimes: Map[String, LocalDateTime] = Map(),
/** Timestamps of when a vehicle or equipment was last purchased */
useTimes: Map[String, LocalDateTime] = Map()
) {
assert(bep >= 0)
assert(cep >= 0)
val br: BattleRank = BattleRank.withExperience(bep)
val cr: CommandRank = CommandRank.withExperience(cep)
private def cooldown(
times: Map[String, LocalDateTime],
cooldowns: Map[BasicDefinition, FiniteDuration],
definition: BasicDefinition
): Option[Period] = {
times.get(definition.Name) match {
case Some(purchaseTime) =>
val secondsSincePurchase = new Period(purchaseTime, LocalDateTime.now()).toStandardSeconds.getSeconds
cooldowns.get(definition) match {
case Some(cooldown) if (cooldown.toSeconds - secondsSincePurchase) > 0 =>
Some(Period.seconds(cooldown.toSeconds.toInt - secondsSincePurchase))
case _ => None
}
case None =>
None
}
}
/** Returns the remaining purchase cooldown or None if an object is not on cooldown */
def purchaseCooldown(definition: BasicDefinition): Option[Period] = {
cooldown(purchaseTimes, Avatar.purchaseCooldowns, definition)
}
/** Returns the remaining use cooldown or None if an object is not on cooldown */
def useCooldown(definition: BasicDefinition): Option[Period] = {
cooldown(useTimes, Avatar.useCooldowns, definition)
}
def fifthSlot(): EquipmentSlot = {
new OffhandEquipmentSlot(EquipmentSize.Inventory) {
val obj = new LockerEquipment(locker)
Equipment = obj
}
}
val definition: AvatarDefinition = GlobalDefinitions.avatar
/** Returns numerical value from 0-3 that is the hacking skill level representation in packets */
def hackingSkillLevel(): Int = {
if (
certifications.contains(Certification.ExpertHacking) || certifications.contains(Certification.ElectronicsExpert)
) {
3
} else if (certifications.contains(Certification.AdvancedHacking)) {
2
} else if (certifications.contains(Certification.Hacking)) {
1
} else {
0
}
}
/** The maximum stamina amount */
val maxStamina: Int = 100
/** Return true if the stamina is at the maximum amount */
def staminaFull: Boolean = {
stamina == maxStamina
}
def canEqual(other: Any): Boolean = other.isInstanceOf[Avatar]
override def equals(other: Any): Boolean =
other match {
case that: Avatar =>
(that canEqual this) &&
id == that.id
case _ =>
false
}
/** Avatar assertions
* These protect against programming errors by asserting avatar properties have correct values
* They may or may not be disabled for live applications
*/
assert(stamina <= maxStamina && stamina >= 0)
assert(head >= 0) // TODO what's the max value?
assert(implants.length <= 3)
assert(implants.flatten.map(_.definition.implantType).distinct.length == implants.flatten.length)
assert(br.implantSlots >= implants.flatten.length)
}

View file

@ -0,0 +1,19 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.avatar
/**
* An `Enumeration` of all the avatar types in the game, paired with their object id as the `Value`.
* #121 is the most important.
*/
object Avatars extends Enumeration {
final val avatar = Value(121)
final val avatar_bot = Value(122)
final val avatar_bot_agile = Value(123)
final val avatar_bot_agile_no_weapon = Value(124)
final val avatar_bot_max = Value(125)
final val avatar_bot_max_no_weapon = Value(126)
final val avatar_bot_reinforced = Value(127)
final val avatar_bot_reinforced_no_weapon = Value(128)
final val avatar_bot_standard = Value(129)
final val avatar_bot_standard_no_weapon = Value(130)
}

View file

@ -0,0 +1,106 @@
package net.psforever.objects.avatar
import enumeratum.values.{IntEnum, IntEnumEntry}
import net.psforever.packet.game.objectcreate.UniformStyle
/** Battle ranks and their starting experience values
* Source: http://wiki.psforever.net/wiki/Battle_Rank
*/
sealed abstract class BattleRank(val value: Int, val experience: Long) extends IntEnumEntry {
def implantSlots: Int = {
if (this.value >= BattleRank.BR18.value) {
3
} else if (this.value >= BattleRank.BR12.value) {
2
} else if (this.value >= BattleRank.BR6.value) {
1
} else {
0
}
}
def uniformStyle: UniformStyle.Value = {
if (this.value >= BattleRank.BR25.value) {
UniformStyle.ThirdUpgrade
} else if (this.value >= BattleRank.BR14.value) {
UniformStyle.SecondUpgrade
} else if (this.value >= BattleRank.BR7.value) {
UniformStyle.FirstUpgrade
} else {
UniformStyle.Normal
}
}
}
case object BattleRank extends IntEnum[BattleRank] {
case object BR1 extends BattleRank(value = 1, experience = 0L)
case object BR2 extends BattleRank(value = 2, experience = 1000L)
case object BR3 extends BattleRank(value = 3, experience = 3000L)
case object BR4 extends BattleRank(value = 4, experience = 7500L)
case object BR5 extends BattleRank(value = 5, experience = 15000L)
case object BR6 extends BattleRank(value = 6, experience = 30000L)
case object BR7 extends BattleRank(value = 7, experience = 45000L)
case object BR8 extends BattleRank(value = 8, experience = 67500L)
case object BR9 extends BattleRank(value = 9, experience = 101250L)
case object BR10 extends BattleRank(value = 10, experience = 126563L)
case object BR11 extends BattleRank(value = 11, experience = 158203L)
case object BR12 extends BattleRank(value = 12, experience = 197754L)
case object BR13 extends BattleRank(value = 13, experience = 247192L)
case object BR14 extends BattleRank(value = 14, experience = 308990L)
case object BR15 extends BattleRank(value = 15, experience = 386239L)
case object BR16 extends BattleRank(value = 16, experience = 482798L)
case object BR17 extends BattleRank(value = 17, experience = 603497L)
case object BR18 extends BattleRank(value = 18, experience = 754371L)
case object BR19 extends BattleRank(value = 19, experience = 942964L)
case object BR20 extends BattleRank(value = 20, experience = 1178705L)
case object BR21 extends BattleRank(value = 21, experience = 1438020L)
case object BR22 extends BattleRank(value = 22, experience = 1710301L)
case object BR23 extends BattleRank(value = 23, experience = 1988027L)
case object BR24 extends BattleRank(value = 24, experience = 2286231L)
case object BR25 extends BattleRank(value = 25, experience = 2583441L)
val values: IndexedSeq[BattleRank] = findValues
/** Find BattleRank variant for given experience value */
def withExperience(experience: Long): BattleRank = {
withExperienceOpt(experience).get
}
/** Find BattleRank variant for given experience value */
def withExperienceOpt(experience: Long): Option[BattleRank] = {
values.find(br =>
this.withValueOpt(br.value + 1) match {
case Some(nextBr) =>
experience >= br.experience && experience < nextBr.experience
case None =>
experience >= br.experience
}
)
}
}

View file

@ -0,0 +1,219 @@
package net.psforever.objects.avatar
import enumeratum.values.{IntEnum, IntEnumEntry}
import net.psforever.packet.PacketHelpers
import scodec.Codec
import scodec.codecs._
import scala.annotation.tailrec
sealed abstract class Certification(
val value: Int,
/** Name used in packets */
val name: String,
/** Certification point cost */
val cost: Int,
val requires: Set[Certification] = Set(),
val replaces: Set[Certification] = Set()
) extends IntEnumEntry
case object Certification extends IntEnum[Certification] {
case object StandardAssault extends Certification(value = 0, name = "standard_assault", cost = 0)
case object MediumAssault extends Certification(value = 1, name = "medium_assault", cost = 2)
case object HeavyAssault
extends Certification(value = 2, name = "heavy_assault", cost = 4, requires = Set(MediumAssault))
case object SpecialAssault
extends Certification(value = 3, name = "special_assault", cost = 3, requires = Set(MediumAssault))
case object AntiVehicular
extends Certification(value = 4, name = "anti_vehicular", cost = 3, requires = Set(MediumAssault))
case object Sniping extends Certification(value = 5, name = "sniper", cost = 3, requires = Set(MediumAssault))
case object EliteAssault
extends Certification(value = 6, name = "special_assault_2", cost = 1, requires = Set(SpecialAssault))
case object AirCavalryScout extends Certification(value = 7, name = "air_cavalry_scout", cost = 3)
case object AirCavalryInterceptor
extends Certification(value = 8, name = "air_cavalry_interceptor", cost = 2, requires = Set(AirCavalryScout))
case object AirCavalryAssault
extends Certification(
value = 9,
name = "air_cavalry_assault",
cost = 2,
requires = Set(AirCavalryScout)
)
case object AirSupport extends Certification(value = 10, name = "air_support", cost = 3)
case object ATV extends Certification(value = 11, name = "quad_all", cost = 1)
case object LightScout
extends Certification(
value = 12,
name = "light_scout",
cost = 5,
replaces = Set(AirCavalryScout, AssaultBuggy, Harasser)
)
case object AssaultBuggy extends Certification(value = 13, name = "assault_buggy", cost = 3, replaces = Set(Harasser))
case object ArmoredAssault1 extends Certification(value = 14, name = "armored_assault1", cost = 2)
case object ArmoredAssault2
extends Certification(value = 15, name = "armored_assault2", cost = 3, requires = Set(ArmoredAssault1))
case object GroundTransport extends Certification(value = 16, name = "ground_transport", cost = 2)
case object GroundSupport extends Certification(value = 17, name = "ground_support", cost = 2)
case object BattleFrameRobotics
extends Certification(value = 18, name = "TODO2", cost = 4, requires = Set(ArmoredAssault2)) // TODO name
case object Flail extends Certification(value = 19, name = "flail", cost = 1, requires = Set(ArmoredAssault2))
case object Switchblade extends Certification(value = 20, name = "switchblade", cost = 1, requires = Set(ATV))
case object Harasser extends Certification(value = 21, name = "harasser", cost = 1)
case object Phantasm extends Certification(value = 22, name = "phantasm", cost = 3, requires = Set(InfiltrationSuit))
case object GalaxyGunship extends Certification(value = 23, name = "gunship", cost = 2, requires = Set(AirSupport))
case object BFRAntiAircraft
extends Certification(value = 24, name = "TODO3", cost = 1, requires = Set(BattleFrameRobotics))
case object BFRAntiInfantry
extends Certification(value = 25, name = "TODO4", cost = 1, requires = Set(BattleFrameRobotics)) // TODO name
case object StandardExoSuit extends Certification(value = 26, name = "TODO5", cost = 0)
case object AgileExoSuit extends Certification(value = 27, name = "agile_armor", cost = 0)
case object ReinforcedExoSuit extends Certification(value = 28, name = "reinforced_armor", cost = 3)
case object InfiltrationSuit extends Certification(value = 29, name = "infiltration_suit", cost = 2)
case object AAMAX extends Certification(value = 30, name = "max_anti_aircraft", cost = 2)
case object AIMAX extends Certification(value = 31, name = "max_anti_personnel", cost = 3)
case object AVMAX extends Certification(value = 32, name = "max_anti_vehicular", cost = 3)
case object UniMAX extends Certification(value = 33, name = "max_all", cost = 6, replaces = Set(AAMAX, AIMAX, AVMAX))
case object Medical extends Certification(value = 34, name = "Medical", cost = 3)
case object AdvancedMedical
extends Certification(value = 35, name = "advanced_medical", cost = 2, requires = Set(Medical))
case object Hacking extends Certification(value = 36, name = "Hacking", cost = 3)
case object AdvancedHacking
extends Certification(value = 37, name = "advanced_hacking", cost = 2, requires = Set(Hacking))
case object ExpertHacking
extends Certification(value = 38, name = "expert_hacking", cost = 2, requires = Set(AdvancedHacking))
case object DataCorruption
extends Certification(value = 39, name = "virus_hacking", cost = 3, requires = Set(AdvancedHacking))
case object ElectronicsExpert
extends Certification(
value = 40,
name = "electronics_expert",
cost = 4,
requires = Set(AdvancedHacking),
replaces = Set(DataCorruption, ExpertHacking)
)
case object Engineering extends Certification(value = 41, name = "Repair", cost = 3)
case object CombatEngineering
extends Certification(value = 42, name = "combat_engineering", cost = 2, requires = Set(Engineering))
case object FortificationEngineering
extends Certification(value = 43, name = "ce_defense", cost = 3, requires = Set(CombatEngineering))
case object AssaultEngineering
extends Certification(value = 44, name = "ce_offense", cost = 3, requires = Set(CombatEngineering))
case object AdvancedEngineering
extends Certification(
value = 45,
name = "ce_advanced",
cost = 5,
requires = Set(CombatEngineering),
replaces = Set(AssaultEngineering, FortificationEngineering)
)
// https://github.com/lloydmeta/enumeratum/issues/86
lazy val values: IndexedSeq[Certification] = findValues
implicit val codec: Codec[Certification] = PacketHelpers.createIntEnumCodec(this, uint8L)
/**
* Certifications are often stored, in object form, as a 46-member collection.
* Encode a subset of certification values for packet form.
*
* @return the certifications, as a single value
*/
def toEncodedLong(certs: Set[Certification]): Long = {
certs
.map { cert => math.pow(2, cert.value).toLong }
.foldLeft(0L)(_ + _)
}
/**
* Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
* Decode a representative value into a subset of certification values.
*
* @see `ChangeSquadMemberRequirementsCertifications`
* @see `changeSquadMemberRequirementsCertificationsCodec`
* @see `fromEncodedLong(Long, Iterable[Long], Set[CertificationType.Value])`
* @param certs the certifications, as a single value
* @return the certifications, as a sequence of values
*/
def fromEncodedLong(certs: Long): Set[Certification] = {
recursiveFromEncodedLong(
certs,
Certification.values.map { cert => math.pow(2, cert.value).toLong }.sorted
)
}
/**
* Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
* Decode a representative value into a subset of certification values
* by repeatedly finding the partition point of values less than a specific one,
* providing for both the next lowest value (to subtract) and an index (of a certification).
*
* @see `ChangeSquadMemberRequirementsCertifications`
* @see `changeSquadMemberRequirementsCertificationsCodec`
* @see `fromEncodedLong(Long)`
* @param certs the certifications, as a single value
* @param splitList the available values to partition
* @param out the accumulating certification values;
* defaults to an empty set
* @return the certifications, as a sequence of values
*/
@tailrec
private def recursiveFromEncodedLong(
certs: Long,
splitList: Iterable[Long],
out: Set[Certification] = Set.empty
): Set[Certification] = {
if (certs == 0 || splitList.isEmpty) {
out
} else {
val (less, _) = splitList.partition(_ <= certs)
recursiveFromEncodedLong(certs - less.last, less, out ++ Set(Certification.withValue(less.size - 1)))
}
}
}

View file

@ -0,0 +1,40 @@
package net.psforever.objects.avatar
import enumeratum.values.{IntEnum, IntEnumEntry}
/** Command ranks and their starting experience values */
sealed abstract class CommandRank(val value: Int, val experience: Long) extends IntEnumEntry
case object CommandRank extends IntEnum[CommandRank] {
case object CR0 extends CommandRank(value = 0, experience = 0L)
case object CR1 extends CommandRank(value = 1, experience = 10000L)
case object CR2 extends CommandRank(value = 2, experience = 50000L)
case object CR3 extends CommandRank(value = 3, experience = 150000L)
case object CR4 extends CommandRank(value = 4, experience = 300000L)
case object CR5 extends CommandRank(value = 5, experience = 600000L)
val values: IndexedSeq[CommandRank] = findValues
/** Find CommandRank variant for given experience value */
def withExperience(experience: Long): CommandRank = {
withExperienceOpt(experience).get
}
/** Find CommandRank variant for given experience value */
def withExperienceOpt(experience: Long): Option[CommandRank] = {
values.find(cr =>
this.withValueOpt(cr.value + 1) match {
case Some(nextCr) =>
experience >= cr.experience && experience < nextCr.experience
case None =>
experience >= cr.experience
}
)
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.avatar
import akka.actor.Actor
import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.packet.game.{ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectDetachMessage}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
class CorpseControl(player: Player) extends Actor with ContainableBehavior {
def ContainerObject = player
//private [this] val log = org.log4s.getLogger(player.Name)
def receive: Receive = containerBehavior.orElse { case _ => ; }
def MessageDeferredCallback(msg: Any): Unit = {
msg match {
case Containable.MoveItem(_, item, _) =>
//momentarily put item back where it was originally
val obj = ContainerObject
obj.Find(item) match {
case Some(slot) =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
)
case None => ;
}
case _ => ;
}
}
def RemoveItemFromSlotCallback(item: Equipment, slot: Int): Unit = {
val obj = ContainerObject
val zone = obj.Zone
val events = zone.AvatarEvents
item.Faction = PlanetSideEmpire.NEUTRAL
events ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID))
}
def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = {
val obj = ContainerObject
val zone = obj.Zone
val events = zone.AvatarEvents
val definition = item.Definition
events ! AvatarServiceMessage(
zone.id,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(obj.GUID, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
)
}
def SwapItemCallback(item: Equipment, fromSlot: Int): Unit = {
val obj = ContainerObject
val zone = obj.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f)
)
)
}
}

View file

@ -0,0 +1,66 @@
package net.psforever.objects.avatar
import enumeratum.values.{IntEnum, IntEnumEntry}
import scodec.{Attempt, Codec}
import scodec.codecs.uint
/** Avatar cosmetic options */
sealed abstract class Cosmetic(val value: Int) extends IntEnumEntry
case object Cosmetic extends IntEnum[Cosmetic] {
case object BrimmedCap extends Cosmetic(value = 1)
case object Earpiece extends Cosmetic(value = 2)
case object Sunglasses extends Cosmetic(value = 4)
case object Beret extends Cosmetic(value = 8)
case object NoHelmet extends Cosmetic(value = 16)
val values: IndexedSeq[Cosmetic] = findValues
/** Get enum values from ObjectCreateMessage value */
def valuesFromObjectCreateValue(value: Int): Set[Cosmetic] = {
values.filter(c => (value & c.value) == c.value).toSet
}
/** Serialize enum values to ObjectCreateMessage value */
def valuesToObjectCreateValue(values: Set[Cosmetic]): Int = {
values.foldLeft(0)(_ + _.value)
}
/** Get enum values from AttributeMessage value
* Attribute and object create messages use different indexes and the NoHelmet value becomes a YesHelmet value
*/
def valuesFromAttributeValue(value: Long): Set[Cosmetic] = {
var values = Set[Cosmetic]()
if (((value >> 4L) & 1L) == 1L) values += Cosmetic.Beret
if (((value >> 3L) & 1L) == 1L) values += Cosmetic.Earpiece
if (((value >> 2L) & 1L) == 1L) values += Cosmetic.Sunglasses
if (((value >> 1L) & 1L) == 1L) values += Cosmetic.BrimmedCap
if (((value >> 0L) & 1L) == 0L) values += Cosmetic.NoHelmet
values
}
/** Serialize enum values to AttributeMessage value
* Attribute and object create messages use different indexes and the NoHelmet value becomes a YesHelmet value
*/
def valuesToAttributeValue(values: Set[Cosmetic]): Long = {
values.foldLeft(1) {
case (sum, NoHelmet) => sum - 1
case (sum, BrimmedCap) => sum + 2
case (sum, Sunglasses) => sum + 4
case (sum, Earpiece) => sum + 8
case (sum, Beret) => sum + 16
}
}
/** Codec for object create messages */
implicit val codec: Codec[Set[Cosmetic]] = uint(5).exmap(
value => Attempt.Successful(Cosmetic.valuesFromObjectCreateValue(value)),
cosmetics => Attempt.Successful(Cosmetic.valuesToObjectCreateValue(cosmetics))
)
}

View file

@ -0,0 +1,714 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.avatar
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem}
import net.psforever.types.PlanetSideGUID
import scala.collection.mutable
/**
* A class that keeps track - "manages" - deployables that are owned by the avatar.<br>
* <br>
* Deployables belong to the Engineering certification line of certifications.
* `CombatEngineering` and above certifications include permissions for different types of deployables,
* and one unique type of deployable is available through the `GroundSupport`
* and one that also requires `AdvancedHacking`.
* (They are collectively called "ce" for that reason.)
* Not only does the level of certification change the maximum number of deployables that can be managed by type
* but it also influences the maximum number of deployables that can be managed by category.
* Individual deployables are counted by type and category individually in special data structures
* to avoid having to probe the primary list of deployable references whenever a question of quantity is asked.
* As deployables are added and removed, and tracked certifications are added and removed,
* these structures are updated to reflect proper count.
*/
class DeployableToolbox {
/**
* a map of bins for keeping track of the quantities of deployables in a category
* keys: categories, values: quantity storage object
*/
private val categoryCounts =
DeployableCategory.values.toSeq.map(value => { value -> new DeployableToolbox.Bin }).toMap
categoryCounts(DeployableCategory.Telepads).Max = 1024
/**
* a map of bins for keeping track of the quantities of individual deployables
* keys: deployable types, values: quantity storage object
*/
private val deployableCounts = DeployedItem.values.toSeq.map(value => { value -> new DeployableToolbox.Bin }).toMap
deployableCounts(DeployedItem.router_telepad_deployable).Max = 1024
/**
* a map of tracked/owned individual deployables
* keys: categories, values: deployable objects
*/
private val deployableLists =
DeployableCategory.values.toSeq
.map(value => { value -> mutable.ListBuffer[DeployableToolbox.AcceptableDeployable]() })
.toMap
/**
* can only be initialized once
* set during the `Initialization` method primarily, and in `Add` and in `Remove` if not
*/
private var initialized: Boolean = false
/**
* Set up the initial deployable counts by providing certification values to be used in category and unit selection.
*
* @param certifications a group of certifications for the initial values
* @return `true`, if this is the first time and actual "initialization" is performed;
* `false`, otherwise
*/
def Initialize(certifications: Set[Certification]): Boolean = {
if (!initialized) {
DeployableToolbox.Initialize(deployableCounts, categoryCounts, certifications)
initialized = true
true
} else {
false
}
}
/**
* Change the count of deployable units that can be tracked by providing a new certification.
* If the given certification is already factored into the quantities, no changes will occur.
* @param certification the new certification
* @param certificationSet the group of previous certifications being tracked;
* occasionally, important former certification values are required for additional configuration;
* the new certification should already have been added to this group
*/
def AddToDeployableQuantities(
certification: Certification,
certificationSet: Set[Certification]
): Unit = {
initialized = true
DeployableToolbox.AddToDeployableQuantities(deployableCounts, categoryCounts, certification, certificationSet)
}
/**
* Change the count of deployable units that can be tracked
* by designating a certification whose deployables will be removed.
* If the given certification is already factored out of the quantities, no changes will occur.
* @param certification the old certification
* @param certificationSet the group of previous certifications being tracked;
* occasionally, important former certification values are required for additional configuration;
* the new certification should already have been excluded from this group
*/
def RemoveFromDeployableQuantities(
certification: Certification,
certificationSet: Set[Certification]
): Unit = {
initialized = true
DeployableToolbox.RemoveFromDeployablesQuantities(deployableCounts, categoryCounts, certification, certificationSet)
}
/**
* Determine if the given deployable can be managed by this toolbox.
* @see `Valid`
* @see `Available`
* @see `Contains`
* @param obj the deployable
* @return `true`, if it can be managed under the current conditions;
* `false`, otherwise
*/
def Accept(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
Valid(obj) && Available(obj) && !Contains(obj)
}
/**
* Determine if the given deployable can be managed by this toolbox
* by testing if the specific deployable maximum and the deployable category maximum is non-zero
* @param obj the deployable
* @return `true`, if both category maximum and deployable type maximum are positive non-zero integers;
* `false`, otherwise
*/
def Valid(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Max > 0 &&
categoryCounts(obj.Definition.DeployCategory).Max > 0
}
/**
* Determine if the given deployable can be managed by this toolbox
* by testing if the specific deployable list and the deployable category list have available slots.
* In this case, a "slot" is merely the difference between the current count is less than the maximum count.
* @param obj the deployable
* @return `true`, if the deployable can be added to the support lists and counted;
* `false`, otherwise
*/
def Available(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Available() &&
categoryCounts(obj.Definition.DeployCategory).Available()
}
/**
* Check if this deployable is already being managed by the toolbox
* by determining whether or not it is already being managed by this toolbox.
* @param obj the deployable
* @return `true`, if the deployable can be found in one of the lists;
* `false`, otherwise
*/
def Contains(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
deployableLists(obj.Definition.DeployCategory).contains(obj)
}
/**
* Manage the provided deployable.<br>
* <br>
* Although proper testing should be performed prior to attempting to add the deployable to this toolbox,
* three tests are administered to determine whether space is available prior to insertion.
* The first two tests check for available space in the category count and in the unit count
* and the third test checks whether the deployable is already being managed by this toolbox.
* No changes should occur if the deployable is not properly added.
* @param obj the deployable
* @return `true`, if the deployable is added;
* `false`, otherwise
*/
def Add(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
val category = obj.Definition.DeployCategory
val dCategory = categoryCounts(category)
val dType = deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item))
val dList = deployableLists(category)
if (dCategory.Available() && dType.Available() && !dList.contains(obj)) {
dCategory.Current += 1
dType.Current += 1
dList += obj
true
} else {
false
}
}
/**
* Stop managing the provided deployable.<br>
* <br>
* Although proper testing should be performed prior to attempting to remove the deployable to this toolbox,
* a single test is administered to determine whether the removal can take place.
* If the deployable is found to currently being managed by this toolbox, then it is properly removed.
* No changes should occur if the deployable is not properly removed.
* @param obj the deployable
* @return `true`, if the deployable is added;
* `false`, otherwise
*/
def Remove(obj: DeployableToolbox.AcceptableDeployable): Boolean = {
val category = obj.Definition.DeployCategory
val deployables = deployableLists(category)
if (deployables.contains(obj)) {
categoryCounts(category).Current -= 1
deployableCounts(DeployableToolbox.UnifiedType(obj.Definition.Item)).Current -= 1
deployables -= obj
true
} else {
false
}
}
/**
* Remove the first managed deployable that matches the same type of deployable as the example.
* The explicit tests is defined to find the first deployable whose type matches.
* @param obj the example deployable
* @return any deployable that is found
*/
def DisplaceFirst(obj: DeployableToolbox.AcceptableDeployable): Option[DeployableToolbox.AcceptableDeployable] = {
DisplaceFirst(obj, { d => d.Definition.Item == obj.Definition.Item })
}
/**
* Remove the first managed deployable that satisfies a test and belongs to the same category as the example.
* The test in question is used to pinpoint the first qualifying deployable;
* but, if the test fails to find any valid targets,
* the first deployable in the list of managed deployables for that category is selected to be removed.
* The only test performed is whether there is any valid deployable managed for the category.
* @param obj the example deployable
* @param rule the testing rule for determining a valid deployable
* @return any deployable that is found
*/
def DisplaceFirst(
obj: DeployableToolbox.AcceptableDeployable,
rule: (Deployable) => Boolean
): Option[DeployableToolbox.AcceptableDeployable] = {
val definition = obj.Definition
val category = definition.DeployCategory
val categoryList = deployableLists(category)
if (categoryList.nonEmpty) {
val found = categoryList.find(rule) match {
case Some(target) =>
categoryList.remove(categoryList.indexOf(target))
case None =>
categoryList.remove(0)
}
categoryCounts(category).Current -= 1
deployableCounts(DeployableToolbox.UnifiedType(found.Definition.Item)).Current -= 1
Some(found)
} else {
None
}
}
/**
* Remove the first managed deployable from a category.
* The only test performed is whether there is any valid deployable managed for the category.
* @param category the target category
* @return any deployable that is found
*/
def DisplaceFirst(category: DeployableCategory.Value): Option[DeployableToolbox.AcceptableDeployable] = {
val categoryList = deployableLists(category)
if (categoryList.nonEmpty) {
val found = categoryList.remove(0)
categoryCounts(category).Current -= 1
deployableCounts(DeployableToolbox.UnifiedType(found.Definition.Item)).Current -= 1
Some(found)
} else {
None
}
}
/**
* Reference all managed deployables of the same type as an example deployable.
* @param filter the example deployable
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def Deployables(filter: DeployableToolbox.AcceptableDeployable): List[PlanetSideGUID] = {
Deployables(filter.Definition.Item)
}
/**
* Reference all managed deployables of the same type.
* @param filter the type of deployable
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def Deployables(filter: DeployedItem.Value): List[PlanetSideGUID] = {
deployableLists(Deployable.Category.Of(filter))
.filter(entry => { entry.Definition.Item == filter })
.map(_.GUID)
.toList
}
/**
* Reference all managed deployables in the same category as an example deployable.
* @param filter the example deployable
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def Category(filter: DeployableToolbox.AcceptableDeployable): List[PlanetSideGUID] = {
Category(filter.Definition.DeployCategory)
}
/**
* Reference all managed deployables in the same category.
* @param filter the type of deployable
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def Category(filter: DeployableCategory.Value): List[PlanetSideGUID] = {
deployableLists(filter).map(_.GUID).toList
}
/**
* Check the current capacity for the same type of deployable as the example.
* @param item the example deployable
* @return the current quantity of deployables and the maximum number
*/
def CountDeployable(item: DeployedItem.Value): (Int, Int) = {
val dType = deployableCounts(DeployableToolbox.UnifiedType(item))
(dType.Current, dType.Max)
}
/**
* Check the current capacity for the same category of deployable as the example.
* @param item the example deployable
* @return the current quantity of deployables and the maximum number
*/
def CountCategory(item: DeployedItem.Value): (Int, Int) = {
val dCat = categoryCounts(Deployable.Category.Of(DeployableToolbox.UnifiedType(item)))
(dCat.Current, dCat.Max)
}
def UpdateUIElement(entry: DeployedItem.Value): List[(Int, Int, Int, Int)] = {
val toEntry = DeployableToolbox.UnifiedType(entry)
val (curr, max) = Deployable.UI(toEntry)
val dType = deployableCounts(toEntry)
List((curr, dType.Current, max, dType.Max))
}
def UpdateUI(): List[(Int, Int, Int, Int)] = DeployedItem.values flatMap UpdateUIElement toList
def UpdateUI(entry: Certification): List[(Int, Int, Int, Int)] = {
import Certification._
entry match {
case AdvancedHacking =>
UpdateUIElement(DeployedItem.sensor_shield)
case CombatEngineering =>
List(
DeployedItem.boomer,
DeployedItem.he_mine,
DeployedItem.spitfire_turret,
DeployedItem.motionalarmsensor
) flatMap UpdateUIElement
case AssaultEngineering =>
List(
DeployedItem.jammer_mine,
DeployedItem.portable_manned_turret,
DeployedItem.deployable_shield_generator
) flatMap UpdateUIElement
case FortificationEngineering =>
List(
DeployedItem.boomer,
DeployedItem.he_mine,
DeployedItem.spitfire_turret,
DeployedItem.spitfire_cloaked,
DeployedItem.spitfire_aa,
DeployedItem.motionalarmsensor,
DeployedItem.tank_traps
) flatMap UpdateUIElement
case AdvancedEngineering =>
List(AssaultEngineering, FortificationEngineering) flatMap UpdateUI
case _ =>
Nil
}
}
def UpdateUI(certifications: List[Certification]): List[(Int, Int, Int, Int)] = {
certifications flatMap UpdateUI
}
/**
* Remove all managed deployables that are the same type.
* @param item the deployable type
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def ClearDeployable(item: DeployedItem.Value): List[PlanetSideGUID] = {
val uitem = DeployableToolbox.UnifiedType(item)
val category = Deployable.Category.Of(uitem)
val categoryList = deployableLists(category)
val (out, in) = categoryList.partition(_.Definition.Item == item)
categoryList.clear()
categoryList ++= in
categoryCounts(category).Current = in.size
deployableCounts(uitem).Current = 0
out.map(_.GUID).toList
}
/**
* Remove all managed deployables that belong to the same category.
* @param item the deployable type belonging to a category
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def ClearCategory(item: DeployedItem.Value): List[PlanetSideGUID] = {
val category = Deployable.Category.Of(DeployableToolbox.UnifiedType(item))
val out = deployableLists(category).map(_.GUID).toList
deployableLists(category).clear()
categoryCounts(category).Current = 0
(Deployable.Category.Includes(category) map DeployableToolbox.UnifiedType toSet)
.foreach({ item: DeployedItem.Value => deployableCounts(item).Current = 0 })
out
}
/**
* Remove all managed deployables.
* @return a list of globally unique identifiers that should be valid for the current zone
*/
def Clear(): List[PlanetSideGUID] = {
val out = deployableLists.values.flatten.map(_.GUID).toList
deployableLists.values.foreach(_.clear())
deployableCounts.values.foreach(_.Current = 0)
categoryCounts.values.foreach(_.Current = 0)
out
}
}
object DeployableToolbox {
/**
* A `type` intended to properly define the minimum acceptable conditions for a `Deployable` object.
*/
type AcceptableDeployable = PlanetSideGameObject with Deployable
/**
* An internal class to keep track of the quantity of deployables managed for a certain set of criteria.
* There are deployable numbers organized by deploybale type and by deployable category.
*/
private class Bin {
/** the maximum number of deployables for this criteria that can be managed */
private var max: Int = 0
/** the current number of deployables for this criteria that are being managed */
private var current: Int = 0
def Current: Int = current
def Current_=(curr: Int): Int = {
current = curr
Current
}
def Max: Int = max
def Max_=(mx: Int): Int = {
max = mx
Max
}
def Available(): Boolean = current < max
}
/**
* Some deployable types, though unique themselves,
* resolve to the same deployable type for the purposes of categorization.
* @param item the type of deployable
* @return the corrected deployable type
*/
def UnifiedType(item: DeployedItem.Value): DeployedItem.Value =
item match {
case DeployedItem.portable_manned_turret_nc | DeployedItem.portable_manned_turret_tr |
DeployedItem.portable_manned_turret_vs =>
DeployedItem.portable_manned_turret
case _ =>
item
}
/**
* Hardcoded maximum values for the category and type initialization.
* @param counts a reference to the type `Bin` object
* @param categories a reference to the category `Bin` object
* @param certifications a group of certifications for the initial values
*/
private def Initialize(
counts: Map[DeployedItem.Value, DeployableToolbox.Bin],
categories: Map[DeployableCategory.Value, DeployableToolbox.Bin],
certifications: Set[Certification]
): Unit = {
import Certification._
if (certifications.contains(AdvancedEngineering)) {
counts(DeployedItem.boomer).Max = 25
counts(DeployedItem.he_mine).Max = 25
counts(DeployedItem.jammer_mine).Max = 20
counts(DeployedItem.spitfire_turret).Max = 15
counts(DeployedItem.spitfire_cloaked).Max = 5
counts(DeployedItem.spitfire_aa).Max = 5
counts(DeployedItem.motionalarmsensor).Max = 25
counts(DeployedItem.tank_traps).Max = 5
counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified
//counts(DeployedItem.portable_manned_turret_nc).Max = 1
//counts(DeployedItem.portable_manned_turret_tr).Max = 1
//counts(DeployedItem.portable_manned_turret_vs).Max = 1
counts(DeployedItem.deployable_shield_generator).Max = 1
categories(DeployableCategory.Boomers).Max = 25
categories(DeployableCategory.Mines).Max = 25
categories(DeployableCategory.SmallTurrets).Max = 15
categories(DeployableCategory.Sensors).Max = 25
categories(DeployableCategory.TankTraps).Max = 5
categories(DeployableCategory.FieldTurrets).Max = 1
categories(DeployableCategory.ShieldGenerators).Max = 1
if (certifications.contains(AdvancedHacking)) {
counts(DeployedItem.sensor_shield).Max = 25
}
} else if (certifications.contains(CombatEngineering)) {
if (certifications.contains(AssaultEngineering)) {
counts(DeployedItem.jammer_mine).Max = 20
counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified
//counts(DeployedItem.portable_manned_turret_nc).Max = 1
//counts(DeployedItem.portable_manned_turret_tr).Max = 1
//counts(DeployedItem.portable_manned_turret_vs).Max = 1
counts(DeployedItem.deployable_shield_generator).Max = 1
categories(DeployableCategory.FieldTurrets).Max = 1
categories(DeployableCategory.ShieldGenerators).Max = 1
}
if (certifications.contains(FortificationEngineering)) {
counts(DeployedItem.boomer).Max = 25
counts(DeployedItem.he_mine).Max = 25
counts(DeployedItem.spitfire_turret).Max = 15
counts(DeployedItem.spitfire_cloaked).Max = 5
counts(DeployedItem.spitfire_aa).Max = 5
counts(DeployedItem.motionalarmsensor).Max = 25
counts(DeployedItem.tank_traps).Max = 5
categories(DeployableCategory.Boomers).Max = 25
categories(DeployableCategory.Mines).Max = 25
categories(DeployableCategory.SmallTurrets).Max = 15
categories(DeployableCategory.Sensors).Max = 25
categories(DeployableCategory.TankTraps).Max = 5
} else {
counts(DeployedItem.boomer).Max = 20
counts(DeployedItem.he_mine).Max = 20
counts(DeployedItem.spitfire_turret).Max = 10
counts(DeployedItem.motionalarmsensor).Max = 20
categories(DeployableCategory.Boomers).Max = 20
categories(DeployableCategory.Mines).Max = 20
categories(DeployableCategory.SmallTurrets).Max = 10
categories(DeployableCategory.Sensors).Max = 20
}
if (certifications.contains(AdvancedHacking)) {
counts(DeployedItem.sensor_shield).Max = 20
}
}
if (certifications.contains(Certification.GroundSupport)) {
counts(DeployedItem.router_telepad_deployable).Max = 1024
categories(DeployableCategory.Telepads).Max = 1024
}
}
/**
* Hardcoded maximum values for the category and type initialization upon providing a new certification.
* @param counts a reference to the type `Bin` object
* @param categories a reference to the category `Bin` object
* @param certification the new certification
* @param certificationSet the group of previous certifications being tracked
*/
def AddToDeployableQuantities(
counts: Map[DeployedItem.Value, DeployableToolbox.Bin],
categories: Map[DeployableCategory.Value, DeployableToolbox.Bin],
certification: Certification,
certificationSet: Set[Certification]
): Unit = {
import Certification._
if (certificationSet contains certification) {
certification match {
case AdvancedHacking =>
if (certificationSet contains CombatEngineering) {
counts(DeployedItem.sensor_shield).Max = 20
}
case CombatEngineering =>
counts(DeployedItem.boomer).Max = 20
counts(DeployedItem.he_mine).Max = 20
counts(DeployedItem.spitfire_turret).Max = 10
counts(DeployedItem.motionalarmsensor).Max = 20
categories(DeployableCategory.Boomers).Max = 20
categories(DeployableCategory.Mines).Max = 20
categories(DeployableCategory.SmallTurrets).Max = 10
categories(DeployableCategory.Sensors).Max = 20
if (certificationSet contains AdvancedHacking) {
counts(DeployedItem.sensor_shield).Max = 20
}
case AssaultEngineering =>
counts(DeployedItem.jammer_mine).Max = 20
counts(DeployedItem.portable_manned_turret).Max = 1 //the below turret types are unified
//counts(DeployedItem.portable_manned_turret_nc).Max = 1
//counts(DeployedItem.portable_manned_turret_tr).Max = 1
//counts(DeployedItem.portable_manned_turret_vs).Max = 1
counts(DeployedItem.deployable_shield_generator).Max = 1
categories(DeployableCategory.FieldTurrets).Max = 1
categories(DeployableCategory.ShieldGenerators).Max = 1
case FortificationEngineering =>
counts(DeployedItem.boomer).Max = 25
counts(DeployedItem.he_mine).Max = 25
counts(DeployedItem.spitfire_turret).Max = 15
counts(DeployedItem.motionalarmsensor).Max = 25
counts(DeployedItem.spitfire_cloaked).Max = 5
counts(DeployedItem.spitfire_aa).Max = 5
counts(DeployedItem.tank_traps).Max = 5
categories(DeployableCategory.Boomers).Max = 25
categories(DeployableCategory.Mines).Max = 25
categories(DeployableCategory.SmallTurrets).Max = 15
categories(DeployableCategory.Sensors).Max = 25
categories(DeployableCategory.TankTraps).Max = 5
case AdvancedEngineering =>
if (!certificationSet.contains(AssaultEngineering)) {
AddToDeployableQuantities(
counts,
categories,
AssaultEngineering,
certificationSet ++ Set(AssaultEngineering)
)
}
if (!certificationSet.contains(FortificationEngineering)) {
AddToDeployableQuantities(
counts,
categories,
FortificationEngineering,
certificationSet ++ Set(FortificationEngineering)
)
}
// case GroundSupport =>
// counts(DeployedItem.router_telepad_deployable).Max = 1024
// categories(DeployableCategory.Telepads).Max = 1024
case _ => ;
}
}
}
/**
* Hardcoded zero'd values for the category and type initialization upon ignoring a previous certification.
* @param counts a reference to the type `Bin` object
* @param categories a reference to the category `Bin` object
* @param certification the new certification
* @param certificationSet the group of previous certifications being tracked
*/
def RemoveFromDeployablesQuantities(
counts: Map[DeployedItem.Value, DeployableToolbox.Bin],
categories: Map[DeployableCategory.Value, DeployableToolbox.Bin],
certification: Certification,
certificationSet: Set[Certification]
): Unit = {
import Certification._
if (!certificationSet.contains(certification)) {
certification match {
case AdvancedHacking =>
counts(DeployedItem.sensor_shield).Max = 0
case CombatEngineering =>
counts(DeployedItem.boomer).Max = 0
counts(DeployedItem.he_mine).Max = 0
counts(DeployedItem.spitfire_turret).Max = 0
counts(DeployedItem.motionalarmsensor).Max = 0
counts(DeployedItem.sensor_shield).Max = 0
categories(DeployableCategory.Boomers).Max = 0
categories(DeployableCategory.Mines).Max = 0
categories(DeployableCategory.SmallTurrets).Max = 0
categories(DeployableCategory.Sensors).Max = 0
case AssaultEngineering =>
counts(DeployedItem.jammer_mine).Max = 0
counts(DeployedItem.portable_manned_turret).Max = 0 //the below turret types are unified
//counts(DeployedItem.portable_manned_turret_nc).Max = 0
//counts(DeployedItem.portable_manned_turret_tr).Max = 0
//counts(DeployedItem.portable_manned_turret_vs).Max = 0
counts(DeployedItem.deployable_shield_generator).Max = 0
categories(DeployableCategory.Sensors).Max = if (certificationSet contains CombatEngineering) 20 else 0
categories(DeployableCategory.FieldTurrets).Max = 0
categories(DeployableCategory.ShieldGenerators).Max = 0
case FortificationEngineering =>
val ce: Int = if (certificationSet contains CombatEngineering) 1 else 0 //true = 1, false = 0
counts(DeployedItem.boomer).Max = ce * 20
counts(DeployedItem.he_mine).Max = ce * 20
counts(DeployedItem.spitfire_turret).Max = ce * 10
counts(DeployedItem.motionalarmsensor).Max = ce * 20
counts(DeployedItem.spitfire_cloaked).Max = 0
counts(DeployedItem.spitfire_aa).Max = 0
counts(DeployedItem.tank_traps).Max = 0
categories(DeployableCategory.Boomers).Max = ce * 20
categories(DeployableCategory.Mines).Max = ce * 20
categories(DeployableCategory.SmallTurrets).Max = ce * 10
categories(DeployableCategory.Sensors).Max = ce * 20
categories(DeployableCategory.TankTraps).Max = 0
case AdvancedEngineering =>
if (!certificationSet.contains(AssaultEngineering)) {
RemoveFromDeployablesQuantities(counts, categories, AssaultEngineering, certificationSet)
}
if (!certificationSet.contains(FortificationEngineering)) {
RemoveFromDeployablesQuantities(counts, categories, FortificationEngineering, certificationSet)
}
// case GroundSupport =>
// counts(DeployedItem.router_telepad_deployable).Max = 0
// categories(DeployableCategory.Telepads).Max = 0
case _ => ;
}
}
}
}

View file

@ -0,0 +1,466 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.avatar
object FirstTimeEvents {
object TR {
val InfantryWeapons: Set[String] = Set(
"used_chainblade",
"used_repeater",
"used_cycler",
"used_mini_chaingun",
"used_striker",
"used_anniversary_guna"
)
val Vehicles: Set[String] = Set(
"used_heavy_grenade_launcher",
"used_apc_tr_weapon",
"used_15mm_chaingun",
"used_105mm_cannon",
"used_colossus_burster",
"used_colossus_chaingun",
"used_colossus_cluster_bomb_pod",
"used_colossus_dual_100mm_cannons",
"used_colossus_tank_cannon",
"visited_threemanheavybuggy",
"visited_battlewagon",
"visited_apc_tr",
"visited_prowler",
"visited_colossus_flight",
"visited_colossus_gunner"
)
val Other: Set[String] = Set(
"used_trhev_dualcycler",
"used_trhev_pounder",
"used_trhev_burster",
"used_colossus_dual_100mm_cannons",
"used_colossus_tank_cannon",
"used_energy_gun_tr",
"visited_portable_manned_turret_tr"
)
val All: Set[String] = InfantryWeapons ++ Vehicles ++ Other
}
object NC {
val InfantryWeapons: Set[String] = Set(
"used_magcutter",
"used_isp",
"used_gauss",
"used_r_shotgun",
"used_hunterseeker",
"used_anniversary_gun"
)
val Vehicles: Set[String] = Set(
"used_firebird",
"used_gauss_cannon",
"used_apc_nc_weapon",
"used_vanguard_weapons",
"used_peregrine_dual_machine_gun",
"used_peregrine_dual_rocket_pods",
"used_peregrine_mechhammer",
"used_peregrine_particle_cannon",
"used_peregrine_sparrow",
"visited_twomanheavybuggy",
"visited_thunderer",
"visited_apc_nc",
"visited_vanguard",
"visited_peregrine_flight",
"visited_peregrine_gunner"
)
val Other: Set[String] = Set(
"used_nchev_scattercannon",
"used_nchev_falcon",
"used_nchev_sparrow",
"used_energy_gun_nc",
"visited_portable_manned_turret_nc"
)
val All: Set[String] = InfantryWeapons ++ Vehicles ++ Other
}
object VS {
val InfantryWeapons: Set[String] = Set(
"used_forceblade",
"used_beamer",
"used_pulsar",
"used_lasher",
"used_lancer",
"used_anniversary_gunb"
)
val Vehicles: Set[String] = Set(
"used_fluxpod",
"used_apc_vs_weapon",
"used_heavy_rail_beam",
"used_pulsed_particle_accelerator",
"used_flux_cannon",
"used_aphelion_laser",
"used_aphelion_starfire",
"used_aphelion_immolation_cannon",
"used_aphelion_plasma_rocket_pod",
"used_aphelion_ppa",
"visited_twomanhoverbuggy",
"visited_aurora",
"visited_apc_vs",
"visited_magrider",
"visited_aphelion_flight",
"visited_aphelion_gunner"
)
val Other: Set[String] = Set(
"used_vshev_quasar",
"used_vshev_comet",
"used_vshev_starfire",
"used_energy_gun_vs",
"visited_portable_manned_turret_vs"
)
val All: Set[String] = InfantryWeapons ++ Vehicles ++ Other
}
object Standard {
val InfantryWeapons: Set[String] = Set(
"used_grenade_plasma",
"used_grenade_jammer",
"used_grenade_frag",
"used_katana",
"used_ilc9",
"used_suppressor",
"used_punisher",
"used_flechette",
"used_phoenix",
"used_thumper",
"used_rocklet",
"used_bolt_driver",
"used_heavy_sniper",
"used_oicw",
"used_flamethrower"
)
val Vehicles: Set[String] = Set(
"used_armor_siphon",
"used_ntu_siphon",
"used_ballgun",
"used_skyguard_weapons",
"used_reaver_weapons",
"used_lightning_weapons",
"used_wasp_weapon_system",
"used_20mm_cannon",
"used_25mm_cannon",
"used_35mm_cannon",
"used_35mm_rotarychaingun",
"used_75mm_cannon",
"used_rotarychaingun",
"used_vulture_bombardier",
"used_vulture_nose_cannon",
"used_vulture_tail_cannon",
"used_liberator_bombardier",
"visited_ams",
"visited_ant",
"visited_quadassault",
"visited_fury",
"visited_quadstealth",
"visited_two_man_assault_buggy",
"visited_skyguard",
"visited_mediumtransport",
"visited_apc",
"visited_lightning",
"visited_mosquito",
"visited_lightgunship",
"visited_wasp",
"visited_liberator",
"visited_vulture",
"visited_dropship",
"visited_galaxy_gunship",
"visited_phantasm",
"visited_lodestar"
)
val Facilities: Set[String] = Set(
"visited_broadcast_warpgate",
"visited_warpgate_small",
"visited_respawn_terminal",
"visited_deconstruction_terminal",
"visited_capture_terminal",
"visited_secondary_capture",
"visited_LLU_socket",
"visited_resource_silo",
"visited_med_terminal",
"visited_adv_med_terminal",
"visited_repair_silo",
"visited_order_terminal",
"visited_certification_terminal",
"visited_implant_terminal",
"visited_locker",
"visited_ground_vehicle_terminal",
"visited_bfr_terminal",
"visited_air_vehicle_terminal",
"visited_galaxy_terminal",
"visited_generator",
"visited_generator_terminal",
"visited_wall_turret",
"used_phalanx",
"used_phalanx_avcombo",
"used_phalanx_flakcombo",
"visited_external_door_lock"
)
val Other: Set[String] = Set(
"used_command_uplink",
"used_med_app",
"used_nano_dispenser",
"used_bank",
"used_ace",
"used_advanced_ace",
"used_rek",
"used_trek",
"used_laze_pointer",
"used_telepad",
"visited_motion_sensor",
"visited_sensor_shield",
"visited_spitfire_turret",
"visited_spitfire_cloaked",
"visited_spitfire_aa",
"visited_shield_generator",
"visited_tank_traps"
)
val All: Set[String] = InfantryWeapons ++ Vehicles ++ Facilities ++ Other
}
object Cavern {
val InfantryWeapons: Set[String] = Set(
"used_spiker",
"used_radiator",
"used_maelstrom"
)
val Vehicles: Set[String] = Set(
"used_scythe",
"used_flail_weapon",
"visited_switchblade",
"visited_flail",
"visited_router"
)
val Facilities: Set[String] = Set(
"used_ancient_turret_weapon",
"visited_vanu_control_console",
"visited_ancient_air_vehicle_terminal",
"visited_ancient_equipment_terminal",
"visited_ancient_ground_vehicle_terminal",
"visited_health_crystal",
"visited_repair_crystal",
"visited_vehicle_crystal",
"visited_damage_crystal",
"visited_energy_crystal"
)
val Other: Set[String] = Set(
"visited_vanu_module"
)
val All: Set[String] = InfantryWeapons ++ Vehicles ++ Facilities ++ Other
}
val Maps: Set[String] = Set(
"map01",
"map02",
"map03",
"map04",
"map05",
"map06",
"map07",
"map08",
"map09",
"map10",
"map11",
"map12",
"map13",
"map14",
"map15",
"map16",
"ugd01",
"ugd02",
"ugd03",
"ugd04",
"ugd05",
"ugd06",
"map96",
"map97",
"map98",
"map99"
)
val Monoliths: Set[String] = Set(
"visited_monolith_amerish",
"visited_monolith_ceryshen",
"visited_monolith_cyssor",
"visited_monolith_esamir",
"visited_monolith_forseral",
"visited_monolith_hossin",
"visited_monolith_ishundar",
"visited_monolith_searhus",
"visited_monolith_solsar"
)
val Gingerman: Set[String] = Set(
"visited_gingerman_atar",
"visited_gingerman_dahaka",
"visited_gingerman_hvar",
"visited_gingerman_izha",
"visited_gingerman_jamshid",
"visited_gingerman_mithra",
"visited_gingerman_rashnu",
"visited_gingerman_sraosha",
"visited_gingerman_yazata",
"visited_gingerman_zal"
)
val Sled: Set[String] = Set(
"visited_sled01",
"visited_sled02",
"visited_sled04",
"visited_sled05",
"visited_sled06",
"visited_sled07",
"visited_sled08",
"visited_sled09"
)
val Snowman: Set[String] = Set(
"visited_snowman_amerish",
"visited_snowman_ceryshen",
"visited_snowman_cyssor",
"visited_snowman_esamir",
"visited_snowman_forseral",
"visited_snowman_hossin",
"visited_snowman_ishundar",
"visited_snowman_searhus",
"visited_snowman_solsar"
)
val Charlie: Set[String] = Set(
"visited_charlie01",
"visited_charlie02",
"visited_charlie03",
"visited_charlie04",
"visited_charlie05",
"visited_charlie06",
"visited_charlie07",
"visited_charlie08",
"visited_charlie09"
)
val BattleRanks: Set[String] = Set(
"xpe_battle_rank_1",
"xpe_battle_rank_2",
"xpe_battle_rank_3",
"xpe_battle_rank_4",
"xpe_battle_rank_5",
"xpe_battle_rank_6",
"xpe_battle_rank_7",
"xpe_battle_rank_8",
"xpe_battle_rank_9",
"xpe_battle_rank_10",
"xpe_battle_rank_11",
"xpe_battle_rank_12",
"xpe_battle_rank_13",
"xpe_battle_rank_14",
"xpe_battle_rank_15",
"xpe_battle_rank_16",
"xpe_battle_rank_17",
"xpe_battle_rank_18",
"xpe_battle_rank_19",
"xpe_battle_rank_20",
"xpe_battle_rank_21",
"xpe_battle_rank_22",
"xpe_battle_rank_23",
"xpe_battle_rank_24",
"xpe_battle_rank_25",
"xpe_battle_rank_26",
"xpe_battle_rank_27",
"xpe_battle_rank_28",
"xpe_battle_rank_29",
"xpe_battle_rank_30",
"xpe_battle_rank_31",
"xpe_battle_rank_32",
"xpe_battle_rank_33",
"xpe_battle_rank_34",
"xpe_battle_rank_35",
"xpe_battle_rank_36",
"xpe_battle_rank_37",
"xpe_battle_rank_38",
"xpe_battle_rank_39",
"xpe_battle_rank_40"
)
val CommandRanks: Set[String] = Set(
"xpe_command_rank_1",
"xpe_command_rank_2",
"xpe_command_rank_3",
"xpe_command_rank_4",
"xpe_command_rank_5"
)
val Training: Set[String] = Set(
"training_welcome",
"training_map",
"training_hart",
"training_warpgates",
"training_weapons01",
"training_armors",
"training_healing",
"training_certifications",
"training_inventory",
"training_vehicles",
"training_implants"
)
val OldTraining: Set[String] = Set(
"training_start_tr",
"training_start_nc",
"training_start_vs"
)
val Generic: Set[String] = Set(
"xpe_overhead_map",
"xpe_mail_alert",
"xpe_join_platoon",
"xpe_form_platoon",
"xpe_join_outfit",
"xpe_form_outfit",
"xpe_join_squad",
"xpe_form_squad",
"xpe_blackops",
"xpe_instant_action",
"xpe_orbital_shuttle",
"xpe_drop_pod",
"xpe_sanctuary_help",
"xpe_bind_facility",
"xpe_warp_gate",
"xpe_warp_gate_usage",
"xpe_bind_ams",
"xpe_th_nonsanc",
"xpe_th_ammo",
"xpe_th_firemodes",
"xpe_th_cloak",
"xpe_th_max",
"xpe_th_ant",
"xpe_th_ams",
"xpe_th_ground",
"xpe_th_ground_p",
"xpe_th_air",
"xpe_th_air_p",
"xpe_th_afterburner",
"xpe_th_hover",
"xpe_th_switchblade",
"xpe_th_router",
"xpe_th_flail",
"xpe_th_bfr"
)
}

View file

@ -0,0 +1,16 @@
package net.psforever.objects.avatar
import net.psforever.objects.definition.ImplantDefinition
import net.psforever.packet.game.objectcreate.ImplantEntry
case class Implant(
definition: ImplantDefinition,
active: Boolean = false,
initialized: Boolean = false
//initializationTime: FiniteDuration
) {
def toEntry: ImplantEntry = {
// TODO initialization time?
new ImplantEntry(definition.implantType, None, active)
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.avatar
import net.psforever.objects.loadouts.Loadout
import scala.util.Success
class LoadoutManager(size: Int) {
private val entries: Array[Option[Loadout]] = Array.fill[Option[Loadout]](size)(None)
def SaveLoadout(owner: Any, label: String, line: Int): Unit = {
Loadout.Create(owner, label) match {
case Success(loadout) if entries.length > line =>
entries(line) = Some(loadout)
case _ => ;
}
}
def LoadLoadout(line: Int): Option[Loadout] = entries.lift(line).flatten
def DeleteLoadout(line: Int): Unit = {
if (entries.length > line) {
entries(line) = None
}
}
def Loadouts: Seq[(Int, Loadout)] =
entries.zipWithIndex.collect { case (Some(loadout), index) => (index, loadout) } toSeq
}

View file

@ -0,0 +1,874 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.actors.session.AvatarActor
import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.vital.{PlayerSuicide, Vitality}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.Repairable
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.vital._
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types._
import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import akka.actor.typed
import scala.concurrent.duration._
class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Command])
extends Actor
with JammableBehavior
with Damageable
with ContainableBehavior {
def JammableObject = player
def DamageableObject = player
def ContainerObject = player
private[this] val log = org.log4s.getLogger(player.Name)
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
/** control agency for the player's locker container (dedicated inventory slot #5) */
val lockerControlAgent: ActorRef = {
val locker = player.avatar.locker
locker.Zone = player.Zone
locker.Actor = context.actorOf(
Props(classOf[LockerContainerControl], locker, player.Name),
PlanetSideServerObject.UniqueActorName(locker)
)
}
override def postStop(): Unit = {
lockerControlAgent ! akka.actor.PoisonPill
player.avatar.locker.Actor = Default.Actor
}
def receive: Receive =
jammableBehavior
.orElse(takesDamage)
.orElse(containerBehavior)
.orElse {
case Player.Die() =>
if (player.isAlive) {
DestructionAwareness(player, None)
}
case CommonMessages.Use(user, Some(item: Tool))
if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive =>
//heal
val originalHealth = player.Health
val definition = player.Definition
if (
player.MaxHealth > 0 && originalHealth < player.MaxHealth &&
user.Faction == player.Faction &&
item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance
) {
val zone = player.Zone
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
if (!(player.isMoving || user.isMoving)) { //only allow stationary heals
val newHealth = player.Health = originalHealth + 10
val magazine = item.Discharge()
events ! AvatarServiceMessage(
uname,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)
)
)
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth))
player.History(
HealFromEquipment(
PlayerSource(player),
PlayerSource(user),
newHealth - originalHealth,
GlobalDefinitions.medicalapplicator
)
)
}
if (player != user) {
//"Someone is trying to heal you"
events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 55, 1))
//progress bar remains visible for all heal attempts
events ! AvatarServiceMessage(
uname,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
RepairMessage(guid, player.Health * 100 / definition.MaxHealth)
)
)
}
}
case CommonMessages.Use(user, Some(item: Tool)) if item.Definition == GlobalDefinitions.medicalapplicator =>
//revive
if (
user != player &&
user.Faction == player.Faction &&
user.isAlive && !user.isMoving &&
!player.isAlive && !player.isBackpack &&
item.Magazine >= 25
) {
sender() ! CommonMessages.Progress(
4,
Players.FinishRevivingPlayer(player, user.Name, item),
Players.RevivingTickAction(player, user, item)
)
}
case CommonMessages.Use(user, Some(item: Tool)) if item.Definition == GlobalDefinitions.bank =>
val originalArmor = player.Armor
val definition = player.Definition
if (
player.MaxArmor > 0 && originalArmor < player.MaxArmor &&
user.Faction == player.Faction &&
item.AmmoType == Ammo.armor_canister && item.Magazine > 0 &&
Vector3.Distance(user.Position, player.Position) < definition.RepairDistance
) {
val zone = player.Zone
val events = zone.AvatarEvents
val uname = user.Name
val guid = player.GUID
if (!(player.isMoving || user.isMoving)) { //only allow stationary repairs
val newArmor = player.Armor =
originalArmor + Repairable.Quality + RepairValue(item) + definition.RepairMod
val magazine = item.Discharge()
events ! AvatarServiceMessage(
uname,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)
)
)
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, player.Armor))
player.History(
RepairFromEquipment(
PlayerSource(player),
PlayerSource(user),
newArmor - originalArmor,
GlobalDefinitions.bank
)
)
}
if (player != user) {
if (player.isAlive) {
//"Someone is trying to repair you" gets strobed twice for visibility
val msg = AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 56, 1))
events ! msg
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(250 milliseconds, events, msg)
}
//progress bar remains visible for all repair attempts
events ! AvatarServiceMessage(
uname,
AvatarAction
.SendResponse(Service.defaultPlayerGUID, RepairMessage(guid, player.Armor * 100 / player.MaxArmor))
)
}
}
case Terminal.TerminalMessage(_, msg, order) =>
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
var toDelete: List[InventoryItem] = Nil
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val definition = player.avatar.faction match {
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX
}
player.avatar.purchaseCooldown(definition) match {
case Some(_) =>
false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(definition)
true
}
} else {
true
})
val result = if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
} else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
} else {
beforeHolsters
}
//populate holsters
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
(
normalHolsters,
Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory
)
} else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
} else {
val (afterHolsters, toInventory) =
normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
(
player
.Holsters()
.zipWithIndex
.map { case (slot, i) => (slot.Equipment, i) }
.collect { case (Some(obj), index) => InventoryItem(obj, index) }
.toList,
remainder
)
}
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
} else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(
a,
b.map {
InventoryItem(_, -1)
}
)
}
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
}
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeExosuit(
player.GUID,
toArmor,
exosuit,
subtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX && requestToChangeArmor,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
stow,
drop,
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
)
)
true
} else {
false
}
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
)
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"wants to change equipment loadout to their option #${msg.unk1 + 1}")
val fallbackSubtype = 0
val fallbackSuit = ExoSuitType.Standard
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
//sanitize exo-suit for change
val dropPred = ContainableBehavior.DropPredicate(player)
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
val dropHolsters = oldHolsters.filter(dropPred)
val oldInventory = player.Inventory.Clear()
val dropInventory = oldInventory.filter(dropPred)
val toDeleteOrDrop: List[InventoryItem] = (player.FreeHand.Equipment match {
case Some(obj) =>
val out = InventoryItem(obj, -1)
player.FreeHand.Equipment = None
if (dropPred(out)) {
List(out)
} else {
Nil
}
case _ =>
Nil
}) ++ dropHolsters ++ dropInventory
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
//imposed 5min delay on mechanized exo-suit switches
val (nextSuit, nextSubtype) =
if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val definition = player.avatar.faction match {
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX
}
player.avatar.purchaseCooldown(definition) match {
case Some(_) => false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(definition)
true
}
} else {
true
})
) {
(exosuit, subtype)
} else {
log.warn(
s"no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead"
)
(fallbackSuit, fallbackSubtype)
}
//sanitize (incoming) inventory
//TODO equipment permissions; these loops may be expanded upon in future
val curatedHolsters = for {
item <- holsters
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
if true
} yield item
val curatedInventory = for {
item <- inventory
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
if true
} yield item
//update suit internally
val originalArmor = player.Armor
player.ExoSuit = nextSuit
val toMaxArmor = player.MaxArmor
val toArmor = if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit))
player.Armor = toMaxArmor
} else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
//a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped
//dropped items are not registered and can just be forgotten
val (afterHolsters, afterInventory) = if (nextSuit == exosuit) {
(
//melee slot preservation for MAX
if (nextSuit == ExoSuitType.MAX) {
holsters.filter(_.start == 4)
} else {
curatedHolsters.filterNot(dropPred)
},
curatedInventory.filterNot(dropPred)
)
} else {
//our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!)
//holsters
val leftoversForInventory = Players.fillEmptyHolsters(
player.Holsters().iterator,
(curatedHolsters ++ curatedInventory).filterNot(dropPred)
)
val finalHolsters = player
.Holsters()
.zipWithIndex
.collect { case (slot, index) if slot.Equipment.nonEmpty => InventoryItem(slot.Equipment.get, index) }
.toList
//inventory
val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory)
(finalHolsters, finalInventory)
}
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeLoadout(
player.GUID,
toArmor,
nextSuit,
nextSubtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX,
oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterInventory,
toDeleteOrDrop
)
)
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)
)
case _ => assert(false, msg.toString)
}
case Zone.Ground.ItemOnGround(item, _, _) =>
val name = player.Name
val zone = player.Zone
val avatarEvents = zone.AvatarEvents
val localEvents = zone.LocalEvents
item match {
case trigger: BoomerTrigger =>
//dropped the trigger, no longer own the boomer; make certain whole faction is aware of that
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match {
case (Some(boomer: BoomerDeployable), Some(avatar)) =>
val guid = boomer.GUID
val factionChannel = boomer.Faction.toString
if (avatar.deployables.Remove(boomer)) {
boomer.Faction = PlanetSideEmpire.NEUTRAL
boomer.AssignOwnership(None)
avatar.deployables.UpdateUIElement(boomer.Definition.Item).foreach {
case (currElem, curr, maxElem, max) =>
avatarEvents ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)
)
avatarEvents ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)
)
}
localEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, zone))
localEvents ! LocalServiceMessage(
factionChannel,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
DeploymentAction.Dismiss,
DeployableInfo(guid, DeployableIcon.Boomer, boomer.Position, PlanetSideGUID(0))
)
)
avatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, PlanetSideEmpire.NEUTRAL)
)
}
case _ => ; //pointless trigger? or a trigger being deleted?
}
case _ => ;
}
case Zone.Ground.CanNotDropItem(_, item, reason) =>
log.warn(s"${player.Name} tried to drop a ${item.Definition.Name} on the ground, but it $reason")
case Zone.Ground.ItemInHand(_) => ;
case Zone.Ground.CanNotPickupItem(_, item_guid, reason) =>
log.warn(s"${player.Name} failed to pick up an item ($item_guid) from the ground because $reason")
case _ => ;
}
protected def TakesDamage: Receive = {
case Vitality.Damage(applyDamageTo) =>
if (player.isAlive && !player.spectator) {
val originalHealth = player.Health
val originalArmor = player.Armor
val originalStamina = player.avatar.stamina
val originalCapacitor = player.Capacitor.toInt
val cause = applyDamageTo(player)
val health = player.Health
val armor = player.Armor
val stamina = player.avatar.stamina
val capacitor = player.Capacitor.toInt
val damageToHealth = originalHealth - health
val damageToArmor = originalArmor - armor
val damageToStamina = originalStamina - stamina
val damageToCapacitor = originalCapacitor - capacitor
HandleDamage(player, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor)
if (damageToHealth > 0 || damageToArmor > 0 || damageToStamina > 0 || damageToCapacitor > 0) {
damageLog.info(
s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalStamina/$originalCapacitor, AFTER=$health/$armor/$stamina/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToStamina/$damageToCapacitor"
)
}
}
}
/**
* na
* @param target na
*/
def HandleDamage(
target: Player,
cause: ResolvedProjectile,
damageToHealth: Int,
damageToArmor: Int,
damageToStamina: Int,
damageToCapacitor: Int
): Unit = {
val targetGUID = target.GUID
val zone = target.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val health = target.Health
if (damageToArmor > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor))
}
if (health > 0) {
if (damageToCapacitor > 0) {
events ! AvatarServiceMessage(
target.Name,
AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong)
)
}
if (damageToHealth > 0 || damageToStamina > 0) {
target.History(cause)
if (damageToHealth > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health))
}
if (damageToStamina > 0) {
avatarActor ! AvatarActor.ConsumeStamina(damageToStamina)
}
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
//alert damage source
DamageAwareness(target, cause)
}
if (Damageable.CanJammer(target, cause)) {
target.Actor ! JammableUnit.Jammered(cause)
}
} else {
DestructionAwareness(target, Some(cause))
}
}
/**
* na
* @param target na
* @param cause na
*/
def DamageAwareness(target: Player, cause: ResolvedProjectile): Unit = {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
cause.projectile.owner match {
case pSource: PlayerSource => //player damage
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) => AvatarAction.HitHint(tplayer.GUID, target.GUID)
case None =>
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position))
}
case source =>
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position))
}
)
}
/**
* The player has lost all his vitality and must be killed.<br>
* <br>
* Shift directly into a state of being dead on the client by setting health to zero points,
* whereupon the player will perform a dramatic death animation.
* Stamina is also set to zero points.
* If the player was in a vehicle at the time of demise, special conditions apply and
* the model must be manipulated so it behaves correctly.
* Do not move or completely destroy the `Player` object as its coordinates of death will be important.<br>
* <br>
* A maximum revive waiting timer is started.
* When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent.
* @param target na
* @param cause na
*/
def DestructionAwareness(target: Player, cause: Option[ResolvedProjectile]): Unit = {
val player_guid = target.GUID
val pos = target.Position
val respawnTimer = 300000 //milliseconds
val zone = target.Zone
val events = zone.AvatarEvents
val nameChannel = target.Name
val zoneChannel = zone.id
target.Die
//unjam
CancelJammeredSound(target)
CancelJammeredStatus(target)
//uninitialize implants
avatarActor ! AvatarActor.DeinitializeImplants()
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.Killed(player_guid, target.VehicleSeated)
) //align client interface fields with state
zone.GUID(target.VehicleSeated) match {
case Some(obj: Mountable) =>
//boot cadaver from seat internally (vehicle perspective)
obj.PassengerInSeat(target) match {
case Some(index) =>
obj.Seats(index).Occupant = None
case _ => ;
}
//boot cadaver from seat on client
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero)
)
)
//make player invisible on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1))
//only the dead player should "see" their own body, so that the death camera has something to focus on
events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid))
case _ => ;
}
events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health
if (target.Capacitor > 0) {
target.Capacitor = 0
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 7, 0)) // capacitor
}
val attribute = cause match {
case Some(resolved) =>
resolved.projectile.owner match {
case pSource: PlayerSource =>
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) => tplayer.GUID
case None => player_guid
}
case _ => player_guid
}
case _ => player_guid
}
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos)
) //how many players get this message?
)
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, true)
)
)
//TODO other methods of death?
val pentry = PlayerSource(target)
(target.History.find({ p => p.isInstanceOf[PlayerSuicide] }) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
cause.orElse { target.LastShot } match {
case out @ Some(shot) =>
if (System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
out
} else {
None //suicide
}
case None =>
None //suicide
}
}) match {
case Some(shot) =>
events ! AvatarServiceMessage(
zoneChannel,
AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to)
)
case None =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
}
/**
* Start the jammered buzzing.
* Although, as a rule, the jammering sound effect should last as long as the jammering status,
* Infantry seem to hear the sound for a bit longer than the effect.
* @see `JammableBehavior.StartJammeredSound`
* @param target an object that can be affected by the jammered status
* @param dur the duration of the timer, in milliseconds;
* by default, 30000
*/
override def StartJammeredSound(target: Any, dur: Int): Unit =
target match {
case obj: Player if !jammedSound =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
obj.Zone.id,
AvatarAction.PlanetsideAttributeToAll(obj.GUID, 27, 1)
)
super.StartJammeredSound(obj, 3000)
case _ => ;
}
/**
* Perform a variety of tasks to indicate being jammered.
* Deactivate implants (should also uninitialize them),
* delay stamina regeneration for a certain number of turns,
* and set the jammered status on specific holstered equipment.
* @see `JammableBehavior.StartJammeredStatus`
* @param target an object that can be affected by the jammered status
* @param dur the duration of the timer, in milliseconds
*/
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
avatarActor ! AvatarActor.DeinitializeImplants()
avatarActor ! AvatarActor.SuspendStaminaRegeneration(5 seconds)
super.StartJammeredStatus(target, dur)
}
override def CancelJammeredStatus(target: Any): Unit = {
avatarActor ! AvatarActor.InitializeImplants(instant = true)
super.CancelJammeredStatus(target)
}
/**
* Stop the jammered buzzing.
* @see `JammableBehavior.CancelJammeredSound`
* @param target an object that can be affected by the jammered status
*/
override def CancelJammeredSound(target: Any): Unit =
target match {
case obj: Player if jammedSound =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
obj.Zone.id,
AvatarAction.PlanetsideAttributeToAll(obj.GUID, 27, 0)
)
super.CancelJammeredSound(obj)
case _ => ;
}
def RepairValue(item: Tool): Int =
if (player.ExoSuit != ExoSuitType.MAX) {
item.FireMode.Add.Damage0
} else {
item.FireMode.Add.Damage3
}
def MessageDeferredCallback(msg: Any): Unit = {
msg match {
case Containable.MoveItem(_, item, _) =>
//momentarily put item back where it was originally
val obj = ContainerObject
obj.Find(item) match {
case Some(slot) =>
obj.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectAttachMessage(obj.GUID, item.GUID, slot))
)
case None => ;
}
case _ => ;
}
}
def RemoveItemFromSlotCallback(item: Equipment, slot: Int): Unit = {
val obj = ContainerObject
val zone = obj.Zone
val events = zone.AvatarEvents
val toChannel = if (obj.VisibleSlots.contains(slot)) zone.id else player.Name
item.Faction = PlanetSideEmpire.NEUTRAL
if (slot == obj.DrawnSlot) {
obj.DrawnSlot = Player.HandsDownSlot
}
events ! AvatarServiceMessage(toChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID))
}
def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = {
val obj = ContainerObject
val guid = obj.GUID
val zone = obj.Zone
val events = zone.AvatarEvents
val name = player.Name
val definition = item.Definition
val faction = obj.Faction
item.Faction = faction
events ! AvatarServiceMessage(
name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(guid, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
)
if (obj.VisibleSlots.contains(slot)) {
events ! AvatarServiceMessage(zone.id, AvatarAction.EquipmentInHand(guid, guid, slot, item))
}
//handle specific types of items
item match {
case trigger: BoomerTrigger =>
//pick up the trigger, own the boomer; make certain whole faction is aware of that
(zone.GUID(trigger.Companion), zone.Players.find { _.name == name }) match {
case (Some(boomer: BoomerDeployable), Some(avatar))
if !boomer.OwnerName.contains(name) || boomer.Faction != faction =>
val bguid = boomer.GUID
val faction = player.Faction
val factionChannel = faction.toString
if (avatar.deployables.Add(boomer)) {
boomer.Faction = faction
boomer.AssignOwnership(player)
avatar.deployables.UpdateUIElement(boomer.Definition.Item).foreach {
case (currElem, curr, maxElem, max) =>
events ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, maxElem, max)
)
events ! AvatarServiceMessage(
name,
AvatarAction.PlanetsideAttributeToAll(Service.defaultPlayerGUID, currElem, curr)
)
}
zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(boomer), zone))
events ! AvatarServiceMessage(
factionChannel,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, bguid, faction)
)
zone.LocalEvents ! LocalServiceMessage(
factionChannel,
LocalAction.DeployableMapIcon(
Service.defaultPlayerGUID,
DeploymentAction.Build,
DeployableInfo(
bguid,
DeployableIcon.Boomer,
boomer.Position,
boomer.Owner.getOrElse(PlanetSideGUID(0))
)
)
)
}
case _ => ; //pointless trigger?
}
case _ => ;
}
}
def SwapItemCallback(item: Equipment, fromSlot: Int): Unit = {
val obj = ContainerObject
val zone = obj.Zone
val toChannel = if (obj.VisibleSlots.contains(fromSlot)) zone.id else player.Name
zone.AvatarEvents ! AvatarServiceMessage(
toChannel,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID)
)
}
}

View file

@ -0,0 +1,55 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.TurretDeployable
import net.psforever.objects.ce.ComplexDeployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class ComplexDeployableSource(
obj_def: ObjectDefinition with DeployableDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
ownerName: String,
position: Vector3,
orientation: Vector3
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def Shields = shields
def OwnerName = ownerName
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
}
object ComplexDeployableSource {
def apply(obj: ComplexDeployable): ComplexDeployableSource = {
ComplexDeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation
)
}
def apply(obj: TurretDeployable): ComplexDeployableSource = {
ComplexDeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation
)
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class DeployableSource(
obj_def: ObjectDefinition with DeployableDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
ownerName: String,
position: Vector3,
orientation: Vector3
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def OwnerName = ownerName
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
}
object DeployableSource {
def apply(obj: PlanetSideGameObject with Deployable): DeployableSource = {
DeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.OwnerName.getOrElse(""),
obj.Position,
obj.Orientation
)
}
}

View file

@ -0,0 +1,85 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class ObjectSource(
obj: PlanetSideGameObject,
faction: PlanetSideEmpire.Value,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3]
) extends SourceEntry {
private val definition = obj.Definition match {
case vital : VitalityDefinition => vital
case genericDefinition => NonvitalDefinition(genericDefinition)
}
private val modifiers = definition match {
case nonvital : NonvitalDefinition => nonvital
case _ => ObjectSource.FixedResistances
}
override def Name = SourceEntry.NameFormat(obj.Definition.Name)
override def Faction = faction
def Definition = definition
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object ObjectSource {
final val FixedResistances = new ResistanceProfileMutators() { }
def apply(obj: PlanetSideGameObject): ObjectSource = {
ObjectSource(
obj,
obj match {
case aligned: FactionAffinity => aligned.Faction
case _ => PlanetSideEmpire.NEUTRAL
},
obj.Position,
obj.Orientation,
obj.Velocity
)
}
}
/**
* A wrapper for a definition that does not represent a `Vitality` object.
* @param definition the original definition
*/
class NonvitalDefinition(private val definition : ObjectDefinition)
extends ObjectDefinition(definition.ObjectId)
with ResistanceProfileMutators
with VitalityDefinition {
Name = { definition.Name }
Packet = { definition.Packet }
def canEqual(a: Any) : Boolean = a.isInstanceOf[definition.type]
override def equals(that: Any): Boolean = definition.equals(that)
override def hashCode: Int = definition.hashCode
}
object NonvitalDefinition {
//single point of contact for all wrapped definitions
private val storage: scala.collection.mutable.LongMap[NonvitalDefinition] =
new scala.collection.mutable.LongMap[NonvitalDefinition]()
def apply(definition : ObjectDefinition) : NonvitalDefinition = {
storage.get(definition.ObjectId) match {
case Some(existing) =>
existing
case None =>
val out = new NonvitalDefinition(definition)
storage += definition.ObjectId.toLong -> out
out
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Player
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, Vector3}
final case class PlayerSource(
name: String,
char_id: Long,
obj_def: AvatarDefinition,
faction: PlanetSideEmpire.Value,
exosuit: ExoSuitType.Value,
seated: Boolean,
health: Int,
armor: Int,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3],
modifiers: ResistanceProfile
) extends SourceEntry {
override def Name = name
override def Faction = faction
override def CharId = char_id
def Definition = obj_def
def ExoSuit = exosuit
def Seated = seated
def Health = health
def Armor = armor
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object PlayerSource {
def apply(tplayer: Player): PlayerSource = {
PlayerSource(
tplayer.Name,
tplayer.CharId,
tplayer.Definition,
tplayer.Faction,
tplayer.ExoSuit,
tplayer.VehicleSeated.nonEmpty,
tplayer.Health,
tplayer.Armor,
tplayer.Position,
tplayer.Orientation,
tplayer.Velocity,
ExoSuitDefinition.Select(tplayer.ExoSuit, tplayer.Faction)
)
}
}

View file

@ -0,0 +1,126 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition}
import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.FireModeDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.types.Vector3
/**
* A summation of weapon (`Tool`) discharge.
* @see `ProjectileDefinition`<br>
* `ToolDefinition`<br>
* `FireModeDefinition`<br>
* `SourceEntry`<br>
* `PlayerSource`
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile;
* most often a player (`PlayerSource`)
* @param attribute_to an object ID that refers to the method of death that would be reported;
* usually the same as `tool_def.ObjectId`;
* if not, then it is a type of vehicle (and owner should have a positive `seated` field)
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @param fire_time when the weapon discharged was recorded;
* defaults to `System.nanoTime`
*/
final case class Projectile(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
fire_mode: FireModeDefinition,
owner: SourceEntry,
attribute_to: Int,
shot_origin: Vector3,
shot_angle: Vector3,
fire_time: Long = System.nanoTime
) extends PlanetSideGameObject {
Position = shot_origin
Orientation = shot_angle
Velocity = {
val initVel: Int = profile.InitialVelocity //initial velocity
val radAngle: Double = math.toRadians(shot_angle.y) //angle of elevation
val rise: Float = initVel * math.sin(radAngle).toFloat //z
val ground: Float = initVel * math.cos(radAngle).toFloat //base
Vector3.Rz(Vector3(0, -ground, 0), shot_angle.z) + Vector3.z(rise)
}
/** Information about the current world coordinates and orientation of the projectile */
val current: SimpleWorldEntity = new SimpleWorldEntity()
private var resolved: ProjectileResolution.Value = ProjectileResolution.Unresolved
/**
* Mark the projectile as being "encountered" or "managed" at least once.
*/
def Resolve(): Unit = {
resolved = ProjectileResolution.Resolved
}
def Miss(): Unit = {
resolved = ProjectileResolution.MissedShot
}
def isResolved: Boolean = resolved == ProjectileResolution.Resolved || resolved == ProjectileResolution.MissedShot
def isMiss: Boolean = resolved == ProjectileResolution.MissedShot
def Definition = profile
}
object Projectile {
/** the first projectile GUID used by all clients internally */
final val baseUID: Int = 40100
/** all clients progress through 40100 to 40124 normally, skipping only for long-lived projectiles
* 40125 to 40149 are being reserved as a guard against undetected overflow
*/
final val rangeUID: Int = 40150
/**
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
def apply(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
fire_mode: FireModeDefinition,
owner: PlanetSideGameObject with FactionAffinity,
shot_origin: Vector3,
shot_angle: Vector3
): Projectile = {
Projectile(profile, tool_def, fire_mode, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle)
}
/**
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param attribute_to an object ID that refers to the method of death that would be reported
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
def apply(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
fire_mode: FireModeDefinition,
owner: PlanetSideGameObject with FactionAffinity,
attribute_to: Int,
shot_origin: Vector3,
shot_angle: Vector3
): Projectile = {
Projectile(profile, tool_def, fire_mode, SourceEntry(owner), attribute_to, shot_origin, shot_angle)
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
/**
* An `Enumeration` of outcomes regarding what actually happened to the projectile.
*/
object ProjectileResolution extends Enumeration {
type Type = Value
val Unresolved, //original basic non-resolution
MissedShot, //projectile did not encounter any collision object and was despawned
Resolved, //a general "projectile encountered something" status with a more specific resolution
Hit, //direct hit, one target
Splash, //area of effect damage, potentially multiple targets
Lash //lashing damage, potentially multiple targets
= Value
}

View file

@ -0,0 +1,149 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
/**
* An `Enumeration` of all the projectile types in the game, paired with their object id as the `Value`.
*/
object Projectiles extends Enumeration {
final val no_projectile = Value(0)
final val bullet_105mm_projectile = Value(1)
final val bullet_12mm_projectile = Value(4)
final val bullet_12mm_projectileb = Value(5)
final val bullet_150mm_projectile = Value(7)
final val bullet_15mm_apc_projectile = Value(10)
final val bullet_15mm_projectile = Value(11)
final val bullet_20mm_apc_projectile = Value(17)
final val bullet_20mm_projectile = Value(18)
final val bullet_25mm_projectile = Value(20)
final val bullet_35mm_projectile = Value(22)
final val bullet_75mm_apc_projectile = Value(26)
final val bullet_75mm_projectile = Value(27)
final val bullet_9mm_AP_projectile = Value(30)
final val bullet_9mm_projectile = Value(31)
final val anniversary_projectilea = Value(58)
final val anniversary_projectileb = Value(59)
final val aphelion_immolation_cannon_projectile = Value(87)
final val aphelion_laser_projectile = Value(91)
final val aphelion_plasma_rocket_projectile = Value(99)
final val aphelion_ppa_projectile = Value(103)
final val aphelion_starfire_projectile = Value(108)
final val bolt_projectile = Value(147)
final val burster_projectile = Value(155)
final val chainblade_projectile = Value(176)
final val colossus_100mm_projectile = Value(181)
final val colossus_burster_projectile = Value(188)
final val colossus_chaingun_projectile = Value(193)
final val colossus_cluster_bomb_projectile = Value(197)
final val colossus_tank_cannon_projectile = Value(207)
final val comet_projectile = Value(210)
final val dualcycler_projectile = Value(266)
final val dynomite_projectile = Value(268)
final val energy_cell_projectile = Value(273)
final val energy_gun_nc_projectile = Value(277)
final val energy_gun_tr_projectile = Value(279)
final val energy_gun_vs_projectile = Value(281)
final val enhanced_energy_cell_projectile = Value(282)
final val enhanced_quasar_projectile = Value(283)
final val falcon_projectile = Value(286)
final val firebird_missile_projectile = Value(288)
final val flail_projectile = Value(296)
final val flamethrower_fireball = Value(302)
final val flamethrower_projectile = Value(303)
final val flux_cannon_apc_projectile = Value(305)
final val flux_cannon_thresher_projectile = Value(308)
final val fluxpod_projectile = Value(311)
final val forceblade_projectile = Value(325)
final val frag_cartridge_projectile = Value(328)
final val frag_cartridge_projectile_b = Value(329)
final val frag_grenade_projectile = Value(332)
final val frag_grenade_projectile_enh = Value(333)
final val galaxy_gunship_gun_projectile = Value(341)
final val gauss_cannon_projectile = Value(348)
final val grenade_projectile = Value(372)
final val heavy_grenade_projectile = Value(392)
final val heavy_rail_beam_projectile = Value(395)
final val heavy_sniper_projectile = Value(397)
final val hellfire_projectile = Value(400)
final val hunter_seeker_missile_dumbfire = Value(404)
final val hunter_seeker_missile_projectile = Value(405)
final val jammer_cartridge_projectile = Value(414)
final val jammer_cartridge_projectile_b = Value(415)
final val jammer_grenade_projectile = Value(418)
final val jammer_grenade_projectile_enh = Value(419)
final val katana_projectile = Value(422)
final val katana_projectileb = Value(423)
final val lancer_projectile = Value(427)
final val lasher_projectile = Value(430)
final val lasher_projectile_ap = Value(431)
final val liberator_bomb_cluster_bomblet_projectile = Value(436)
final val liberator_bomb_cluster_projectile = Value(437)
final val liberator_bomb_projectile = Value(438)
final val maelstrom_grenade_projectile = Value(465)
final val maelstrom_grenade_projectile_contact = Value(466)
final val maelstrom_stream_projectile = Value(467)
final val magcutter_projectile = Value(469)
final val melee_ammo_projectile = Value(541)
final val meteor_common = Value(543)
final val meteor_projectile_b_large = Value(544)
final val meteor_projectile_b_medium = Value(545)
final val meteor_projectile_b_small = Value(546)
final val meteor_projectile_large = Value(547)
final val meteor_projectile_medium = Value(548)
final val meteor_projectile_small = Value(549)
final val mine_projectile = Value(551)
final val mine_sweeper_projectile = Value(554)
final val mine_sweeper_projectile_enh = Value(555)
final val oicw_little_buddy = Value(601)
final val oicw_projectile = Value(602)
final val pellet_gun_projectile = Value(631)
final val peregrine_dual_machine_gun_projectile = Value(639)
final val peregrine_mechhammer_projectile = Value(647)
final val peregrine_particle_cannon_projectile = Value(654)
final val peregrine_rocket_pod_projectile = Value(657)
final val peregrine_sparrow_projectile = Value(661)
final val phalanx_av_projectile = Value(665)
final val phalanx_flak_projectile = Value(667)
final val phalanx_projectile = Value(669)
final val phoenix_missile_guided_projectile = Value(675)
final val phoenix_missile_projectile = Value(676)
final val plasma_cartridge_projectile = Value(678)
final val plasma_cartridge_projectile_b = Value(679)
final val plasma_grenade_projectile = Value(682)
final val plasma_grenade_projectile_B = Value(683)
final val pounder_projectile = Value(694)
final val pounder_projectile_enh = Value(695)
final val ppa_projectile = Value(696)
final val pulsar_ap_projectile = Value(702)
final val pulsar_projectile = Value(703)
final val quasar_projectile = Value(713)
final val radiator_grenade_projectile = Value(718)
final val radiator_sticky_projectile = Value(719)
final val reaver_rocket_projectile = Value(723)
final val rocket_projectile = Value(735)
final val rocklet_flak_projectile = Value(738)
final val rocklet_jammer_projectile = Value(739)
final val scattercannon_projectile = Value(746)
final val scythe_projectile = Value(748)
final val scythe_projectile_slave = Value(749)
final val shotgun_shell_AP_projectile = Value(757)
final val shotgun_shell_projectile = Value(758)
final val six_shooter_projectile = Value(763)
final val skyguard_flak_cannon_projectile = Value(787)
final val sparrow_projectile = Value(792)
final val sparrow_secondary_projectile = Value(793)
final val spiker_projectile = Value(818)
final val spitfire_aa_ammo_projectile = Value(821)
final val spitfire_ammo_projectile = Value(824)
final val starfire_projectile = Value(831)
final val striker_missile_projectile = Value(840)
final val striker_missile_targeting_projectile = Value(841)
final val trek_projectile = Value(878)
final val vanu_sentry_turret_projectile = Value(944)
final val vulture_bomb_projectile = Value(988)
final val vulture_nose_bullet_projectile = Value(989)
final val vulture_tail_bullet_projectile = Value(991)
final val wasp_gun_projectile = Value(999)
final val wasp_rocket_projectile = Value(1001)
final val winchester_projectile = Value(1005)
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.vital.DamageResistanceModel
import net.psforever.types.Vector3
/**
* An encapsulation of a projectile event that records sufficient historical information
* about the interaction of weapons discharge and a target
* to the point that the original event might be reconstructed.
* Reenacting the calculations of this entry should always produce the same values.
* @param resolution how the projectile hit was executed
* @param projectile the original projectile
* @param target what the projectile hit
* @param damage_model the kind of damage model to which the `target` is/was subject
* @param hit_pos where the projectile hit
*/
final case class ResolvedProjectile(
resolution: ProjectileResolution.Value,
projectile: Projectile,
target: SourceEntry,
damage_model: DamageResistanceModel,
hit_pos: Vector3
) {
val hit_time: Long = System.nanoTime
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.ce.{ComplexDeployable, SimpleDeployable}
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
trait SourceEntry extends WorldEntity {
def Name: String = ""
def Definition: ObjectDefinition with VitalityDefinition
def CharId: Long = 0L
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def Position_=(pos: Vector3) = Position
def Orientation_=(pos: Vector3) = Position
def Velocity_=(pos: Option[Vector3]) = Velocity
def Modifiers: ResistanceProfile
}
object SourceEntry {
final val None = new SourceEntry() {
def Definition = null
def Position = Vector3.Zero
def Orientation = Vector3.Zero
def Velocity = Some(Vector3.Zero)
def Modifiers = null
}
def apply(target: PlanetSideGameObject with FactionAffinity): SourceEntry = {
target match {
case obj: Player => PlayerSource(obj)
case obj: Vehicle => VehicleSource(obj)
case obj: ComplexDeployable => ComplexDeployableSource(obj)
case obj: SimpleDeployable => DeployableSource(obj)
case _ => ObjectSource(target)
}
}
def NameFormat(name: String): String = {
name
.replace("_", " ")
.split(" ")
.map(_.capitalize)
.mkString(" ")
}
}

View file

@ -0,0 +1,43 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Vehicle
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class VehicleSource(
obj_def: VehicleDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3],
modifiers: ResistanceProfile
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition: VehicleDefinition = obj_def
def Health = health
def Shields = shields
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object VehicleSource {
def apply(obj: Vehicle): VehicleSource = {
VehicleSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.Position,
obj.Orientation,
obj.Velocity,
obj.Definition.asInstanceOf[ResistanceProfile]
)
}
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import net.psforever.objects.definition.ComplexDeployableDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
abstract class ComplexDeployable(cdef: ComplexDeployableDefinition) extends PlanetSideServerObject with Deployable {
private var shields: Int = 0
def Shields: Int = shields
def Shields_=(toShields: Int): Int = {
shields = math.min(math.max(0, toShields), MaxShields)
Shields
}
def MaxShields: Int = {
0 //Definition.MaxShields
}
def Definition = cdef
}

View file

@ -0,0 +1,110 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import net.psforever.objects._
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.DeployableIcon
import net.psforever.types.PlanetSideEmpire
trait Deployable extends FactionAffinity with Vitality with OwnableByPlayer with ZoneAware {
this: PlanetSideGameObject =>
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def MaxHealth: Int
def Faction: PlanetSideEmpire.Value = faction
override def Faction_=(toFaction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = toFaction
Faction
}
def DamageModel: DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel]
def Definition: DeployableDefinition
}
object Deployable {
object Category {
def Of(item: DeployedItem.Value): DeployableCategory.Value = deployablesToCategories(item)
def Includes(category: DeployableCategory.Value): List[DeployedItem.Value] = {
(for {
(ce, cat) <- deployablesToCategories
if cat == category
} yield ce) toList
}
def OfAll(): Map[DeployedItem.Value, DeployableCategory.Value] = deployablesToCategories
private val deployablesToCategories: Map[DeployedItem.Value, DeployableCategory.Value] = Map(
DeployedItem.boomer -> DeployableCategory.Boomers,
DeployedItem.he_mine -> DeployableCategory.Mines,
DeployedItem.jammer_mine -> DeployableCategory.Mines,
DeployedItem.spitfire_turret -> DeployableCategory.SmallTurrets,
DeployedItem.motionalarmsensor -> DeployableCategory.Sensors,
DeployedItem.spitfire_cloaked -> DeployableCategory.SmallTurrets,
DeployedItem.spitfire_aa -> DeployableCategory.SmallTurrets,
DeployedItem.deployable_shield_generator -> DeployableCategory.ShieldGenerators,
DeployedItem.tank_traps -> DeployableCategory.TankTraps,
DeployedItem.portable_manned_turret -> DeployableCategory.FieldTurrets,
DeployedItem.portable_manned_turret_nc -> DeployableCategory.FieldTurrets,
DeployedItem.portable_manned_turret_tr -> DeployableCategory.FieldTurrets,
DeployedItem.portable_manned_turret_vs -> DeployableCategory.FieldTurrets,
DeployedItem.sensor_shield -> DeployableCategory.Sensors,
DeployedItem.router_telepad_deployable -> DeployableCategory.Telepads
)
}
object Icon {
def apply(item: DeployedItem.Value): DeployableIcon.Value = ceicon(item.id)
private val ceicon: Map[Int, DeployableIcon.Value] = Map(
DeployedItem.boomer.id -> DeployableIcon.Boomer,
DeployedItem.he_mine.id -> DeployableIcon.HEMine,
DeployedItem.jammer_mine.id -> DeployableIcon.DisruptorMine,
DeployedItem.spitfire_turret.id -> DeployableIcon.SpitfireTurret,
DeployedItem.spitfire_cloaked.id -> DeployableIcon.ShadowTurret,
DeployedItem.spitfire_aa.id -> DeployableIcon.CerebusTurret,
DeployedItem.motionalarmsensor.id -> DeployableIcon.MotionAlarmSensor,
DeployedItem.sensor_shield.id -> DeployableIcon.SensorDisruptor,
DeployedItem.tank_traps.id -> DeployableIcon.TRAP,
DeployedItem.portable_manned_turret.id -> DeployableIcon.FieldTurret,
DeployedItem.portable_manned_turret_tr.id -> DeployableIcon.FieldTurret,
DeployedItem.portable_manned_turret_nc.id -> DeployableIcon.FieldTurret,
DeployedItem.portable_manned_turret_vs.id -> DeployableIcon.FieldTurret,
DeployedItem.deployable_shield_generator.id -> DeployableIcon.AegisShieldGenerator,
DeployedItem.router_telepad_deployable.id -> DeployableIcon.RouterTelepad
).withDefaultValue(DeployableIcon.Boomer)
}
object UI {
def apply(item: DeployedItem.Value): (Int, Int) = planetsideAttribute(item)
/**
* The attribute values to be invoked in `PlanetsideAttributeMessage` packets
* in reference to a particular combat engineering deployable element on the UI.
* The first number is for the actual count field.
* The second number is for the maximum count field.
*/
private val planetsideAttribute: Map[DeployedItem.Value, (Int, Int)] = Map(
DeployedItem.boomer -> (94, 83),
DeployedItem.he_mine -> (95, 84),
DeployedItem.jammer_mine -> (96, 85),
DeployedItem.spitfire_turret -> (97, 86),
DeployedItem.motionalarmsensor -> (98, 87),
DeployedItem.spitfire_cloaked -> (99, 88),
DeployedItem.spitfire_aa -> (100, 89),
DeployedItem.deployable_shield_generator -> (101, 90),
DeployedItem.tank_traps -> (102, 91),
DeployedItem.portable_manned_turret -> (103, 92),
DeployedItem.portable_manned_turret_nc -> (103, 92),
DeployedItem.portable_manned_turret_tr -> (103, 92),
DeployedItem.portable_manned_turret_vs -> (103, 92),
DeployedItem.sensor_shield -> (104, 93)
).withDefaultValue((0, 0))
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
object DeployableCategory extends Enumeration {
type Type = Value
val Boomers, Mines, SmallTurrets, Sensors, TankTraps, FieldTurrets, ShieldGenerators, Telepads = Value
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
object DeployedItem extends Enumeration {
type Type = Value
final val boomer = Value(148)
final val deployable_shield_generator = Value(240)
final val he_mine = Value(388)
final val jammer_mine = Value(420) //disruptor mine
final val motionalarmsensor = Value(575)
final val sensor_shield = Value(752) //sensor disruptor
final val spitfire_aa = Value(819) //cerebus turret
final val spitfire_cloaked = Value(825) //shadow turret
final val spitfire_turret = Value(826)
final val tank_traps = Value(849) //trap
final val portable_manned_turret = Value(685)
final val portable_manned_turret_nc = Value(686)
final val portable_manned_turret_tr = Value(687)
final val portable_manned_turret_vs = Value(688)
final val router_telepad_deployable = Value(744)
}

View file

@ -0,0 +1,11 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.SimpleDeployableDefinition
abstract class SimpleDeployable(cdef: SimpleDeployableDefinition) extends PlanetSideGameObject with Deployable {
Health = Definition.MaxHealth
def Definition = cdef
}

View file

@ -0,0 +1,102 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ce
import akka.actor.ActorContext
import net.psforever.objects.{Default, PlanetSideGameObject, TelepadDeployable, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.zones.Zone
import net.psforever.types.PlanetSideGUID
trait TelepadLike {
private var router: Option[PlanetSideGUID] = None
private var activated: Boolean = false
def Router: Option[PlanetSideGUID] = router
def Router_=(rguid: PlanetSideGUID): Option[PlanetSideGUID] = Router_=(Some(rguid))
def Router_=(rguid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
router match {
case None =>
router = rguid
case Some(_) =>
if (rguid.isEmpty || rguid.contains(PlanetSideGUID(0))) {
router = None
}
}
Router
}
def Active: Boolean = activated
def Active_=(state: Boolean): Boolean = {
activated = state
Active
}
}
object TelepadLike {
final case class Activate(obj: PlanetSideGameObject with TelepadLike)
final case class Deactivate(obj: PlanetSideGameObject with TelepadLike)
/**
* Assemble some logic for a provided object.
* @param obj an `Amenity` object;
* anticipating a `Terminal` object using this same definition
* @param context hook to the local `Actor` system
*/
def Setup(obj: Amenity, context: ActorContext): Unit = {
obj.asInstanceOf[TelepadLike].Router = obj.Owner.GUID
import akka.actor.Props
if (obj.Actor == Default.Actor) {
obj.Actor = context.actorOf(Props(classOf[TelepadControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
/**
* An analysis of the active system of teleportation utilized by Router vehicles.
* Information about the two endpoints - an internal telepad and a remote telepad - are collected, if they are applicable.
* The vehicle "Router" itself must be in the drive state of `Deployed`.
* @param router the vehicle that serves as the container of an internal telepad unit
* @param zone where the router is located
* @return the pair of units that compose the teleportation system
*/
def AppraiseTeleportationSystem(router: Vehicle, zone: Zone): Option[(Utility.InternalTelepad, TelepadDeployable)] = {
import net.psforever.objects.vehicles.UtilityType
import net.psforever.types.DriveState
router.Utility(UtilityType.internal_router_telepad_deployable) match {
//if the vehicle has an internal telepad, it is allowed to be a Router (that's a weird way of saying it)
case Some(util: Utility.InternalTelepad) =>
//check for a readied remote telepad
zone.GUID(util.Telepad) match {
case Some(telepad: TelepadDeployable) =>
//determine whether to activate both the Router's internal telepad and the deployed remote telepad
if (router.DeploymentState == DriveState.Deployed && util.Active && telepad.Active) {
Some((util, telepad))
} else {
None
}
case _ =>
None
}
case _ =>
None
}
}
}
/**
* Telepad-like components don't actually use control agents right now, but,
* since the `trait` is used for a `Vehicle` `Utility` entity as well as a `Deployable` entity,
* and all utilities are supposed to have control agents with which to interface,
* a placeholder like this is easy to reason around.
* @param obj an entity that extends `TelepadLike`
*/
class TelepadControl(obj: TelepadLike) extends akka.actor.Actor {
def receive: akka.actor.Actor.Receive = {
case _ => ;
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.definition.converter.AmmoBoxConverter
import net.psforever.objects.equipment.Ammo
class AmmoBoxDefinition(objectId: Int) extends EquipmentDefinition(objectId) {
import net.psforever.objects.equipment.EquipmentSize
private val ammoType: Ammo.Value = Ammo(objectId) //let throw NoSuchElementException
private var capacity: Int = 1
Name = "ammo box"
Size = EquipmentSize.Inventory
Packet = AmmoBoxDefinition.converter
def AmmoType: Ammo.Value = ammoType
def Capacity: Int = capacity
def Capacity_=(capacity: Int): Int = {
this.capacity = capacity
Capacity
}
}
object AmmoBoxDefinition {
private val converter = new AmmoBoxConverter()
def apply(objectId: Int): AmmoBoxDefinition = {
new AmmoBoxDefinition(objectId)
}
def apply(ammoType: Ammo.Value): AmmoBoxDefinition = {
new AmmoBoxDefinition(ammoType.id)
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.avatar.Avatars
import net.psforever.objects.definition.converter.AvatarConverter
import net.psforever.objects.vital.VitalityDefinition
/**
* The definition for game objects that look like other people, and also for players.
* @param objectId the object's identifier number
*/
class AvatarDefinition(objectId: Int) extends ObjectDefinition(objectId) with VitalityDefinition {
Avatars(objectId) //let throw NoSuchElementException
Packet = AvatarDefinition.converter
}
object AvatarDefinition {
private val converter = new AvatarConverter()
def apply(objectId: Int): AvatarDefinition = {
new AvatarDefinition(objectId)
}
def apply(avatar: Avatars.Value): AvatarDefinition = {
new AvatarDefinition(avatar.id)
}
}

View file

@ -0,0 +1,23 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
abstract class BasicDefinition {
private var name: String = "definition"
private var descriptor: Option[String] = None
def Name: String = name
def Name_=(name: String): String = {
this.name = name
Name
}
def Descriptor: String = descriptor.getOrElse(Name)
def Descriptor_=(description: String): String = Descriptor_=(Some(description))
def Descriptor_=(description: Option[String]): String = {
descriptor = description
Descriptor
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.vehicles.CargoVehicleRestriction
/**
* The definition for a cargo hold.
*/
class CargoDefinition extends BasicDefinition {
/** a restriction on the type of exo-suit a person can wear */
private var vehicleRestriction: CargoVehicleRestriction.Value = CargoVehicleRestriction.Small
/** the user can escape while the vehicle is moving */
private var bailable: Boolean = true
Name = "cargo"
def CargoRestriction: CargoVehicleRestriction.Value = {
this.vehicleRestriction
}
def CargoRestriction_=(restriction: CargoVehicleRestriction.Value): CargoVehicleRestriction.Value = {
this.vehicleRestriction = restriction
restriction
}
def Bailable: Boolean = {
this.bailable
}
def Bailable_=(canBail: Boolean): Boolean = {
this.bailable = canBail
canBail
}
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.avatar.Certification
import net.psforever.objects.ce.DeployedItem
import net.psforever.objects.definition.converter.ACEConverter
import net.psforever.objects.equipment.CItem
import scala.collection.mutable.ListBuffer
class ConstructionItemDefinition(objectId: Int) extends EquipmentDefinition(objectId) {
CItem(objectId) //let throw NoSuchElementException
private val modes: ListBuffer[ConstructionFireMode] = ListBuffer()
Packet = new ACEConverter
def Modes: ListBuffer[ConstructionFireMode] = modes
}
object ConstructionItemDefinition {
def apply(objectId: Int): ConstructionItemDefinition = {
new ConstructionItemDefinition(objectId)
}
def apply(cItem: CItem.Value): ConstructionItemDefinition = {
new ConstructionItemDefinition(cItem.id)
}
}
class ConstructionFireMode {
private val deployables: ListBuffer[DeployedItem.Value] = ListBuffer.empty
private val permissions: ListBuffer[Set[Certification]] = ListBuffer.empty
def Permissions: ListBuffer[Set[Certification]] = permissions
def Deployables: ListBuffer[DeployedItem.Value] = deployables
def Item(deployable: DeployedItem.Value): ListBuffer[DeployedItem.Value] = {
deployables += deployable
permissions += Set.empty[Certification]
deployables
}
def Item(deployable: DeployedItem.Value, permission: Set[Certification]): ListBuffer[DeployedItem.Value] = {
deployables += deployable
permissions += permission
deployables
}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.inventory.InventoryTile
/**
* The definition for any piece of `Equipment`.
* @param objectId the object's identifier number
*/
abstract class EquipmentDefinition(objectId: Int) extends ObjectDefinition(objectId) {
/** the size of the item when placed in an EquipmentSlot / holster / mounting */
private var size: EquipmentSize.Value = EquipmentSize.Blocked
/** the size of the item when placed in the grid inventory space */
private var tile: InventoryTile = InventoryTile.Tile11
/** a correction for the z-coordinate for some dropped items to avoid sinking into the ground */
private var dropOffset: Float = 0f
def Size: EquipmentSize.Value = size
def Size_=(newSize: EquipmentSize.Value): EquipmentSize.Value = {
size = newSize
Size
}
def Tile: InventoryTile = tile
def Tile_=(newTile: InventoryTile): InventoryTile = {
tile = newTile
Tile
}
def DropOffset: Float = dropOffset
def DropOffset(offset: Float): Float = {
dropOffset = offset
DropOffset
}
}

View file

@ -0,0 +1,196 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.Certification
import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.vital._
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
/**
* A definition for producing the personal armor the player wears.
* Players are influenced by the exo-suit they wear in a variety of ways, with speed and available equipment slots being major differences.
* @param suitType the `Enumeration` corresponding to this exo-suit
*/
class ExoSuitDefinition(private val suitType: ExoSuitType.Value)
extends BasicDefinition
with ResistanceProfileMutators
with DamageResistanceModel {
protected var permissions: List[Certification] = List.empty
protected var maxArmor: Int = 0
protected val holsters: Array[EquipmentSize.Value] = Array.fill[EquipmentSize.Value](5)(EquipmentSize.Blocked)
protected var inventoryScale: InventoryTile = InventoryTile.Tile11 //override with custom InventoryTile
protected var inventoryOffset: Int = 0
protected var maxCapacitor: Int = 0
protected var capacitorRechargeDelayMillis: Int = 0
protected var capacitorRechargePerSecond: Int = 0
protected var capacitorDrainPerSecond: Int = 0
Name = "exo-suit"
DamageUsing = DamageCalculations.AgainstExoSuit
ResistUsing = StandardInfantryResistance
Model = StandardResolutions.Infantry
def SuitType: ExoSuitType.Value = suitType
def MaxArmor: Int = maxArmor
def MaxArmor_=(armor: Int): Int = {
maxArmor = math.min(math.max(0, armor), 65535)
MaxArmor
}
def MaxCapacitor: Int = maxCapacitor
def MaxCapacitor_=(value: Int): Int = {
maxCapacitor = value
maxCapacitor
}
def CapacitorRechargeDelayMillis: Int = capacitorRechargeDelayMillis
def CapacitorRechargeDelayMillis_=(value: Int): Int = {
capacitorRechargeDelayMillis = value
capacitorRechargeDelayMillis
}
def CapacitorRechargePerSecond: Int = capacitorRechargePerSecond
def CapacitorRechargePerSecond_=(value: Int): Int = {
capacitorRechargePerSecond = value
capacitorRechargePerSecond
}
def CapacitorDrainPerSecond: Int = capacitorDrainPerSecond
def CapacitorDrainPerSecond_=(value: Int): Int = {
capacitorDrainPerSecond = value
capacitorDrainPerSecond
}
def InventoryScale: InventoryTile = inventoryScale
def InventoryScale_=(scale: InventoryTile): InventoryTile = {
inventoryScale = scale
InventoryScale
}
def InventoryOffset: Int = inventoryOffset
def InventoryOffset_=(offset: Int): Int = {
inventoryOffset = math.min(math.max(0, offset), 65535)
InventoryOffset
}
def Holsters: Array[EquipmentSize.Value] = holsters
def Holster(slot: Int): EquipmentSize.Value = {
if (slot >= 0 && slot < 5) {
holsters(slot)
} else {
EquipmentSize.Blocked
}
}
def Holster(slot: Int, value: EquipmentSize.Value): EquipmentSize.Value = {
if (slot >= 0 && slot < 5) {
holsters(slot) = value
holsters(slot)
} else {
EquipmentSize.Blocked
}
}
def Permissions: List[Certification] = permissions
def Permissions_=(certs: List[Certification]): List[Certification] = {
permissions = certs
Permissions
}
def Use: ExoSuitDefinition = this
override def hashCode(): Int = {
val state = Seq(suitType)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
}
class SpecialExoSuitDefinition(private val suitType: ExoSuitType.Value) extends ExoSuitDefinition(suitType) {
Name = "heavy_armor"
Descriptor = "heavy_armor"
private var activatedSpecial: SpecialExoSuitDefinition.Mode.Value = SpecialExoSuitDefinition.Mode.Normal
def UsingSpecial: SpecialExoSuitDefinition.Mode.Value = activatedSpecial
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value = {
activatedSpecial = state
UsingSpecial
}
override def Use: ExoSuitDefinition = {
val obj = new SpecialExoSuitDefinition(SuitType)
obj.Permissions = Permissions
obj.MaxArmor = MaxArmor
obj.MaxCapacitor = MaxCapacitor
obj.CapacitorRechargePerSecond = CapacitorRechargePerSecond
obj.CapacitorDrainPerSecond = CapacitorDrainPerSecond
obj.CapacitorRechargeDelayMillis = CapacitorRechargeDelayMillis
obj.InventoryScale = InventoryScale
obj.InventoryOffset = InventoryOffset
obj.Subtract.Damage0 = Subtract.Damage0
obj.Subtract.Damage1 = Subtract.Damage1
obj.Subtract.Damage2 = Subtract.Damage2
obj.Subtract.Damage3 = Subtract.Damage3
obj.ResistanceDirectHit = ResistanceDirectHit
obj.ResistanceSplash = ResistanceSplash
obj.ResistanceAggravated = ResistanceAggravated
obj.DamageUsing = DamageUsing
obj.ResistUsing = ResistUsing
obj.Model = Model
(0 until 5).foreach(index => { obj.Holster(index, Holster(index)) })
obj
}
}
object SpecialExoSuitDefinition {
def apply(suitType: ExoSuitType.Value): SpecialExoSuitDefinition = {
new SpecialExoSuitDefinition(suitType)
}
object Mode extends Enumeration {
type Type = Value
val Normal, Anchored, Overdrive, Shielded = Value
}
}
object ExoSuitDefinition {
def apply(suitType: ExoSuitType.Value): ExoSuitDefinition = {
new ExoSuitDefinition(suitType)
}
/**
* A function to retrieve the correct defintion of an exo-suit from the type of exo-suit.
* @param suit the `Enumeration` corresponding to this exo-suit
* @param faction the faction the player belongs to for this exosuit
* @return the exo-suit definition
*/
def Select(suit: ExoSuitType.Value, faction: PlanetSideEmpire.Value): ExoSuitDefinition = {
suit match {
case ExoSuitType.Infiltration => GlobalDefinitions.Infiltration.Use
case ExoSuitType.Agile => GlobalDefinitions.Agile.Use
case ExoSuitType.Reinforced => GlobalDefinitions.Reinforced.Use
case ExoSuitType.MAX =>
faction match {
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX.Use
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX.Use
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX.Use
}
case _ => GlobalDefinitions.Standard.Use
}
}
}

View file

@ -0,0 +1,87 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.types.{ExoSuitType, ImplantType}
import scala.collection.mutable
/**
* The definition for an installable player utility that grants a perk, usually in exchange for stamina (energy).<br>
* <br>
* Most of the definition deals with the costs of activation and operation.
* When activated by the user, an `activationCharge` may be deducted form that user's stamina reserves.
* This does not necessarily have to be a non-zero value.
* Passive implants are always active and thus have no cost.
* After being activated, a non-passive implant consumes a specific amount of stamina at regular intervals
* Some implants will specify a different interval for consuming stamina based on the exo-suit the player is wearing
*
* @param implantType the type of implant that is defined
* @see `ImplantType`
*/
class ImplantDefinition(val implantType: ImplantType) extends BasicDefinition {
/** how long it takes the implant to become ready for activation; is milliseconds */
private var initializationDuration: Long = 0L
/** a passive certification is activated as soon as it is ready (or other condition) */
private var passive: Boolean = false
/** how much turning on the implant costs */
private var activationStaminaCost: Int = 0
/** how much energy does this implant cost to remain activate per interval tick */
private var staminaCost: Int = 0
/**
* How often in milliseconds the stamina cost will be applied, per exo-suit type
* in game_objects.adb.lst each armour type is listed as a numeric identifier
* stamina_consumption_interval = Standard
* stamina_consumption_interval1 = Infil
* stamina_consumption_interval2 = Agile
* stamina_consumption_interval3 = Rexo
* stamina_consumption_interval4 = MAX?
*/
private var costIntervalDefault: Int = 0
private val costIntervalByExoSuit = mutable.HashMap[ExoSuitType.Value, Int]().withDefaultValue(CostIntervalDefault)
Name = "implant"
def InitializationDuration: Long = initializationDuration
def InitializationDuration_=(time: Long): Long = {
initializationDuration = math.max(0, time)
InitializationDuration
}
def Passive: Boolean = passive
def Passive_=(isPassive: Boolean): Boolean = {
passive = isPassive
Passive
}
def ActivationStaminaCost: Int = activationStaminaCost
def ActivationStaminaCost_=(charge: Int): Int = {
activationStaminaCost = math.max(0, charge)
ActivationStaminaCost
}
def StaminaCost: Int = staminaCost
def StaminaCost_=(charge: Int): Int = {
staminaCost = math.max(0, charge)
StaminaCost
}
def CostIntervalDefault: Int = {
costIntervalDefault
}
def CostIntervalDefault_=(interval: Int): Int = {
costIntervalDefault = interval
CostIntervalDefault
}
def GetCostIntervalByExoSuit(exosuit: ExoSuitType.Value): Int =
costIntervalByExoSuit.getOrElse(exosuit, CostIntervalDefault)
def CostIntervalByExoSuitHashMap: mutable.Map[ExoSuitType.Value, Int] = costIntervalByExoSuit
}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.definition.converter.KitConverter
import net.psforever.objects.equipment.Kits
/**
* The definition for a personal one-time-use recovery item.
* @param objectId the object's identifier number
*/
class KitDefinition(objectId: Int) extends EquipmentDefinition(objectId) {
import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.inventory.InventoryTile
Kits(objectId) //let throw NoSuchElementException
Size = EquipmentSize.Inventory
Tile = InventoryTile.Tile42
Name = "kit"
Packet = KitDefinition.converter
}
object KitDefinition {
private val converter = new KitConverter()
def apply(objectId: Int): KitDefinition = {
new KitDefinition(objectId)
}
def apply(kit: Kits.Value): KitDefinition = {
new KitDefinition(kit.id)
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter}
/**
* Associate an object's canned in-game representation with its basic game identification unit.
* The extension of this `class` would identify the common data necessary to construct such a given game object.<br>
* <br>
* The converter transforms a game object that is created by this `ObjectDefinition` into packet data through method-calls.
* The field for this converter is a `PacketConverter`, the superclass for `ObjectCreateConverter`;
* the type of the mutator's parameter is `ObjectCreateConverter` of a wildcard `tparam`;
* and, the accessor return type is `ObjectCreateConverter[PlanetSideGameObject]`, a minimum-true statement.
* The actual type of the converter at a given point, casted or otherwise, is mostly meaningless.
* Casting the external object does not mutate any of the types used by the methods within that object.
* So long as it is an `ObjectCreatePacket`, those methods can be called correctly for a game object of the desired type.
* @param objectId the object's identifier number
*/
abstract class ObjectDefinition(private val objectId: Int) extends BasicDefinition {
/** a data converter for this type of object */
protected var packet: PacketConverter = new ObjectCreateConverter[PlanetSideGameObject]() {}
Name = "object definition"
/**
* Get the conversion object.
* @return
*/
final def Packet: ObjectCreateConverter[PlanetSideGameObject] =
packet.asInstanceOf[ObjectCreateConverter[PlanetSideGameObject]]
/**
* Assign this definition a conversion object.
* @param pkt the new converter
* @return the current converter, after assignment
*/
final def Packet_=(pkt: ObjectCreateConverter[_]): PacketConverter = {
packet = pkt
Packet
}
def ObjectId: Int = objectId
}

View file

@ -0,0 +1,213 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.ballistics.Projectiles
import net.psforever.objects.equipment.JammingUnit
import net.psforever.objects.vital.damage.DamageModifiers
import net.psforever.objects.vital.{DamageType, StandardDamageProfile}
/**
* The definition that outlines the damage-dealing characteristics of any projectile.
* `Tool` objects emit `ProjectileDefinition` objects and that is later wrapped into a `Projectile` object.
* @param objectId the object's identifier number
*/
class ProjectileDefinition(objectId: Int)
extends ObjectDefinition(objectId)
with JammingUnit
with StandardDamageProfile
with DamageModifiers {
private val projectileType: Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException
private var acceleration: Int = 0
private var accelerationUntil: Float = 0f
private var damageType: DamageType.Value = DamageType.None
private var damageTypeSecondary: DamageType.Value = DamageType.None
private var degradeDelay: Float = 1f
private var degradeMultiplier: Float = 1f
private var initialVelocity: Int = 1
private var lifespan: Float = 1f
private var damageAtEdge: Float = 1f
private var damageRadius: Float = 1f
private var lashRadius : Float = 0f
private var useDamage1Subtract: Boolean = false
private var existsOnRemoteClients: Boolean = false //`true` spawns a server-managed object
private var remoteClientData: (Int, Int) =
(0, 0) //artificial values; for ObjectCreateMessage packet (oicw_little_buddy is undefined)
private var damageProxy: Option[Int] = None
private var autoLock: Boolean = false
private var additionalEffect: Boolean = false
private var jammerProjectile: Boolean = false
//derived calculations
private var distanceMax: Float = 0f
private var distanceFromAcceleration: Float = 0f
private var distanceNoDegrade: Float = 0f
private var finalVelocity: Float = 0f
Name = "projectile"
Modifiers = DamageModifiers.DistanceDegrade
def ProjectileType: Projectiles.Value = projectileType
def UseDamage1Subtract: Boolean = useDamage1Subtract
def UseDamage1Subtract_=(useDamage1Subtract: Boolean): Boolean = {
this.useDamage1Subtract = useDamage1Subtract
UseDamage1Subtract
}
def Acceleration: Int = acceleration
def Acceleration_=(accel: Int): Int = {
acceleration = accel
Acceleration
}
def AccelerationUntil: Float = accelerationUntil
def AccelerationUntil_=(accelUntil: Float): Float = {
accelerationUntil = accelUntil
AccelerationUntil
}
def ProjectileDamageType: DamageType.Value = damageType
def ProjectileDamageType_=(damageType1: DamageType.Value): DamageType.Value = {
damageType = damageType1
ProjectileDamageType
}
def ProjectileDamageTypeSecondary: DamageType.Value = damageTypeSecondary
def ProjectileDamageTypeSecondary_=(damageTypeSecondary1: DamageType.Value): DamageType.Value = {
damageTypeSecondary = damageTypeSecondary1
ProjectileDamageTypeSecondary
}
def DegradeDelay: Float = degradeDelay
def DegradeDelay_=(degradeDelay: Float): Float = {
this.degradeDelay = degradeDelay
DegradeDelay
}
def DegradeMultiplier: Float = degradeMultiplier
def DegradeMultiplier_=(degradeMultiplier: Float): Float = {
this.degradeMultiplier = degradeMultiplier
DegradeMultiplier
}
def InitialVelocity: Int = initialVelocity
def InitialVelocity_=(initialVelocity: Int): Int = {
this.initialVelocity = initialVelocity
InitialVelocity
}
def Lifespan: Float = lifespan
def Lifespan_=(lifespan: Float): Float = {
this.lifespan = lifespan
Lifespan
}
def DamageAtEdge: Float = damageAtEdge
def DamageAtEdge_=(damageAtEdge: Float): Float = {
this.damageAtEdge = damageAtEdge
DamageAtEdge
}
def DamageRadius: Float = damageRadius
def DamageRadius_=(damageRadius: Float): Float = {
this.damageRadius = damageRadius
DamageRadius
}
def LashRadius: Float = lashRadius
def LashRadius_=(radius: Float): Float = {
lashRadius = radius
LashRadius
}
def ExistsOnRemoteClients: Boolean = existsOnRemoteClients
def ExistsOnRemoteClients_=(existsOnRemoteClients: Boolean): Boolean = {
this.existsOnRemoteClients = existsOnRemoteClients
ExistsOnRemoteClients
}
def RemoteClientData: (Int, Int) = remoteClientData
def RemoteClientData_=(remoteClientData: (Int, Int)): (Int, Int) = {
this.remoteClientData = remoteClientData
RemoteClientData
}
def DamageProxy : Option[Int] = damageProxy
def DamageProxy_=(proxyObjectId : Int) : Option[Int] = DamageProxy_=(Some(proxyObjectId))
def DamageProxy_=(proxyObjectId : Option[Int]) : Option[Int] = {
damageProxy = proxyObjectId
DamageProxy
}
def AutoLock: Boolean = autoLock
def AutoLock_=(lockState: Boolean): Boolean = {
autoLock = lockState
AutoLock
}
def AdditionalEffect: Boolean = additionalEffect
def AdditionalEffect_=(effect: Boolean): Boolean = {
additionalEffect = effect
AdditionalEffect
}
def JammerProjectile: Boolean = jammerProjectile
def JammerProjectile_=(effect: Boolean): Boolean = {
jammerProjectile = effect
JammerProjectile
}
def DistanceMax: Float = distanceMax //accessor only
def DistanceFromAcceleration: Float = distanceFromAcceleration //accessor only
def DistanceNoDegrade: Float = distanceNoDegrade //accessor only
def FinalVelocity: Float = finalVelocity //accessor only
}
object ProjectileDefinition {
def apply(projectileType: Projectiles.Value): ProjectileDefinition = {
new ProjectileDefinition(projectileType.id)
}
def CalculateDerivedFields(pdef: ProjectileDefinition): Unit = {
val (distanceMax, distanceFromAcceleration, finalVelocity): (Float, Float, Float) = if (pdef.Acceleration == 0) {
(pdef.InitialVelocity * pdef.Lifespan, 0, pdef.InitialVelocity.toFloat)
} else {
val distanceFromAcceleration =
(pdef.AccelerationUntil * pdef.InitialVelocity) + (0.5f * pdef.Acceleration * pdef.AccelerationUntil * pdef.AccelerationUntil)
val finalVelocity = pdef.InitialVelocity + pdef.Acceleration * pdef.AccelerationUntil
val distanceAfterAcceleration = finalVelocity * (pdef.Lifespan - pdef.AccelerationUntil)
(distanceFromAcceleration + distanceAfterAcceleration, distanceFromAcceleration, finalVelocity)
}
pdef.distanceMax = distanceMax
pdef.distanceFromAcceleration = distanceFromAcceleration
pdef.finalVelocity = finalVelocity
pdef.distanceNoDegrade = if (pdef.DegradeDelay == 0f) {
pdef.distanceMax
} else if (pdef.DegradeDelay < pdef.AccelerationUntil) {
(pdef.DegradeDelay * pdef.InitialVelocity) + (0.5f * pdef.Acceleration * pdef.DegradeDelay * pdef.DegradeDelay)
} else {
pdef.distanceFromAcceleration + pdef.finalVelocity * (pdef.DegradeDelay - pdef.AccelerationUntil)
}
}
}

View file

@ -0,0 +1,51 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.vehicles.SeatArmorRestriction
/**
* The definition for a seat.
*/
class SeatDefinition extends BasicDefinition {
/** a restriction on the type of exo-suit a person can wear */
private var armorRestriction: SeatArmorRestriction.Value = SeatArmorRestriction.NoMax
/** the user can escape while the vehicle is moving */
private var bailable: Boolean = false
/** any controlled weapon */
private var weaponMount: Option[Int] = None
Name = "seat"
def ArmorRestriction: SeatArmorRestriction.Value = {
this.armorRestriction
}
def ArmorRestriction_=(restriction: SeatArmorRestriction.Value): SeatArmorRestriction.Value = {
this.armorRestriction = restriction
restriction
}
def Bailable: Boolean = {
this.bailable
}
def Bailable_=(canBail: Boolean): Boolean = {
this.bailable = canBail
canBail
}
def ControlledWeapon: Option[Int] = {
this.weaponMount
}
def ControlledWeapon_=(wep: Int): Option[Int] = {
ControlledWeapon_=(Some(wep))
}
def ControlledWeapon_=(wep: Option[Int]): Option[Int] = {
this.weaponMount = wep
ControlledWeapon
}
}

Some files were not shown because too many files have changed in this diff Show more