mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-20 15:13:35 +00:00
Restructure repository
* Move /common/src to /src * Move services to net.psforever package * Move /pslogin to /server
This commit is contained in:
parent
89a30ae6f6
commit
f4fd78fc5d
958 changed files with 527 additions and 725 deletions
66
src/main/scala/akka/actor/MDCContextAware.scala
Normal file
66
src/main/scala/akka/actor/MDCContextAware.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
24
src/main/scala/net/psforever/IFinalizable.scala
Normal file
24
src/main/scala/net/psforever/IFinalizable.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
1329
src/main/scala/net/psforever/actors/session/AvatarActor.scala
Normal file
1329
src/main/scala/net/psforever/actors/session/AvatarActor.scala
Normal file
File diff suppressed because it is too large
Load diff
934
src/main/scala/net/psforever/actors/session/ChatActor.scala
Normal file
934
src/main/scala/net/psforever/actors/session/ChatActor.scala
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
9532
src/main/scala/net/psforever/actors/session/SessionActor.scala
Normal file
9532
src/main/scala/net/psforever/actors/session/SessionActor.scala
Normal file
File diff suppressed because it is too large
Load diff
175
src/main/scala/net/psforever/actors/zone/BuildingActor.scala
Normal file
175
src/main/scala/net/psforever/actors/zone/BuildingActor.scala
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
130
src/main/scala/net/psforever/actors/zone/ZoneActor.scala
Normal file
130
src/main/scala/net/psforever/actors/zone/ZoneActor.scala
Normal 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
|
||||
}
|
||||
}
|
||||
310
src/main/scala/net/psforever/crypto/CryptoInterface.scala
Normal file
310
src/main/scala/net/psforever/crypto/CryptoInterface.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
358
src/main/scala/net/psforever/login/CryptoSessionActor.scala
Normal file
358
src/main/scala/net/psforever/login/CryptoSessionActor.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
321
src/main/scala/net/psforever/login/LoginSessionActor.scala
Normal file
321
src/main/scala/net/psforever/login/LoginSessionActor.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
484
src/main/scala/net/psforever/login/PacketCodingActor.scala
Normal file
484
src/main/scala/net/psforever/login/PacketCodingActor.scala
Normal 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()
|
||||
}
|
||||
103
src/main/scala/net/psforever/login/Session.scala
Normal file
103
src/main/scala/net/psforever/login/Session.scala
Normal 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)"
|
||||
}
|
||||
}
|
||||
198
src/main/scala/net/psforever/login/SessionRouter.scala
Normal file
198
src/main/scala/net/psforever/login/SessionRouter.scala
Normal 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
|
||||
}
|
||||
}
|
||||
50
src/main/scala/net/psforever/login/TcpListener.scala
Normal file
50
src/main/scala/net/psforever/login/TcpListener.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
79
src/main/scala/net/psforever/login/UdpListener.scala
Normal file
79
src/main/scala/net/psforever/login/UdpListener.scala
Normal 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()
|
||||
}
|
||||
}
|
||||
146
src/main/scala/net/psforever/login/UdpNetworkSimulator.scala
Normal file
146
src/main/scala/net/psforever/login/UdpNetworkSimulator.scala
Normal 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()
|
||||
}
|
||||
651
src/main/scala/net/psforever/login/WorldSession.scala
Normal file
651
src/main/scala/net/psforever/login/WorldSession.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/main/scala/net/psforever/login/psadmin/CmdInternal.scala
Normal file
30
src/main/scala/net/psforever/login/psadmin/CmdInternal.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
17
src/main/scala/net/psforever/login/psadmin/CmdShutdown.scala
Normal file
17
src/main/scala/net/psforever/login/psadmin/CmdShutdown.scala
Normal 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 =>
|
||||
}
|
||||
}
|
||||
187
src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala
Normal file
187
src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)"
|
||||
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
29
src/main/scala/net/psforever/newcodecs/package.scala
Normal file
29
src/main/scala/net/psforever/newcodecs/package.scala
Normal 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)")
|
||||
}
|
||||
7
src/main/scala/net/psforever/objects/Account.scala
Normal file
7
src/main/scala/net/psforever/objects/Account.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package net.psforever.objects
|
||||
|
||||
case class Account(
|
||||
id: Int,
|
||||
name: String,
|
||||
gm: Boolean = false
|
||||
)
|
||||
71
src/main/scala/net/psforever/objects/AmmoBox.scala
Normal file
71
src/main/scala/net/psforever/objects/AmmoBox.scala
Normal 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})"
|
||||
}
|
||||
}
|
||||
22
src/main/scala/net/psforever/objects/BoomerDeployable.scala
Normal file
22
src/main/scala/net/psforever/objects/BoomerDeployable.scala
Normal 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
|
||||
}
|
||||
}
|
||||
9
src/main/scala/net/psforever/objects/BoomerTrigger.scala
Normal file
9
src/main/scala/net/psforever/objects/BoomerTrigger.scala
Normal 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
|
||||
}
|
||||
67
src/main/scala/net/psforever/objects/ConstructionItem.scala
Normal file
67
src/main/scala/net/psforever/objects/ConstructionItem.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
50
src/main/scala/net/psforever/objects/Default.scala
Normal file
50
src/main/scala/net/psforever/objects/Default.scala
Normal 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
|
||||
}
|
||||
175
src/main/scala/net/psforever/objects/Deployables.scala
Normal file
175
src/main/scala/net/psforever/objects/Deployables.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
127
src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
Normal file
127
src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
6978
src/main/scala/net/psforever/objects/GlobalDefinitions.scala
Normal file
6978
src/main/scala/net/psforever/objects/GlobalDefinitions.scala
Normal file
File diff suppressed because it is too large
Load diff
19
src/main/scala/net/psforever/objects/Kit.scala
Normal file
19
src/main/scala/net/psforever/objects/Kit.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
105
src/main/scala/net/psforever/objects/LivePlayerList.scala
Normal file
105
src/main/scala/net/psforever/objects/LivePlayerList.scala
Normal 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>
|
||||
* `LivePlayerList.Add(session, avatar)`<br>
|
||||
* 2) In between the previous two steps, a range of characters may be queried based on provided statistics.<br>
|
||||
* `LivePlayerList.WorldPopulation(...)`<br>
|
||||
* 3) When the user leaves the game entirely, his character's entry is removed from the mapping.<br>
|
||||
* `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
|
||||
}
|
||||
36
src/main/scala/net/psforever/objects/LocalProjectile.scala
Normal file
36
src/main/scala/net/psforever/objects/LocalProjectile.scala
Normal 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()
|
||||
}
|
||||
}
|
||||
125
src/main/scala/net/psforever/objects/LockerContainer.scala
Normal file
125
src/main/scala/net/psforever/objects/LockerContainer.scala
Normal 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
90
src/main/scala/net/psforever/objects/Ntu.scala
Normal file
90
src/main/scala/net/psforever/objects/Ntu.scala
Normal 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
|
||||
}
|
||||
96
src/main/scala/net/psforever/objects/ObjectType.scala
Normal file
96
src/main/scala/net/psforever/objects/ObjectType.scala
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
object ObjectType extends Enumeration {
|
||||
type Value = String
|
||||
|
||||
val AmbientSoundSource = "ambient_sound_source"
|
||||
val Ammunition = "ammunition"
|
||||
val AnimatedBarrier = "animated_barrier"
|
||||
val Applicator = "applicator"
|
||||
val Armor = "armor"
|
||||
val ArmorSiphon = "armor_siphon"
|
||||
val AwardStatistic = "award_statistic"
|
||||
val Avatar = "avatar"
|
||||
val AvatarBot = "avatar_bot"
|
||||
val Ball = "ball"
|
||||
val Bank = "bank"
|
||||
val Barrier = "barrier"
|
||||
val BfrTerminal = "bfr_terminal"
|
||||
val Billboard = "billboard"
|
||||
val Boomer = "boomer"
|
||||
val BoomerTrigger = "boomer_trigger"
|
||||
val Building = "building"
|
||||
val CaptureFlag = "capture_flag"
|
||||
val CaptureFlagSocket = "capture_flag_socket"
|
||||
val CaptureTerminal = "capture_terminal"
|
||||
val CertTerminal = "cert_terminal"
|
||||
val ChainLashDamager = "chain_lash_damager"
|
||||
val Dispenser = "dispenser"
|
||||
val Door = "door"
|
||||
val EmpBlast = "emp_blast"
|
||||
val FrameVehicle = "framevehicle"
|
||||
val Flag = "flag"
|
||||
val FlightVehicle = "flightvehicle"
|
||||
val ForceDome = "forcedome"
|
||||
val ForceDomeGenerator = "forcedomegenerator"
|
||||
val Game = "game"
|
||||
val Generic = "generic"
|
||||
val GenericTeleportion = "generic_teleportation"
|
||||
val GeneratorTerminal = "generator_terminal"
|
||||
val GsGenbase = "GS_genbase"
|
||||
val HandGrenade = "hand_grenade"
|
||||
val HeMine = "he_mine"
|
||||
val HeavyWeapon = "heavy_weapon"
|
||||
val HoverVehicle = "hovervehicle"
|
||||
val Implant = "implant"
|
||||
val ImplantInterfaceTerminal = "implant_terminal_interface"
|
||||
val Lazer = "lazer"
|
||||
val Locker = "locker"
|
||||
val LockerContainer = "locker_container"
|
||||
val LockExternal = "lock_external"
|
||||
val LockSmall = "lock_small"
|
||||
val MainTerminal = "main_terminal"
|
||||
val Map = "map"
|
||||
val MedicalTerminal = "medical_terminal"
|
||||
val Medkit = "medkit"
|
||||
val Monolith = "monolith"
|
||||
val MonolithUnit = "monolith_unit"
|
||||
val MotionAlarmSensorDest = "motion_alarm_sensor_dest"
|
||||
val NanoDispenser = "nano_dispenser"
|
||||
val NtuSipon = "ntu_siphon"
|
||||
val OrbitalShuttlePad = "orbital_shuttle_pad"
|
||||
val OrbitalStrike = "orbital_strike"
|
||||
val OrderTerminal = "order_terminal"
|
||||
val PainTerminal = "pain_terminal"
|
||||
val Projectile = "projectile"
|
||||
val RadiationCloud = "radiation_cloud"
|
||||
val RearmTerminal = "rearm_terminal"
|
||||
val RechargeTerminal = "recharge_terminal"
|
||||
val Rek = "rek"
|
||||
val RepairTerminal = "repair_terminal"
|
||||
val ResourceSilo = "resource_silo"
|
||||
val RespawnTube = "respawn_tube"
|
||||
val SensorShield = "sensor_shield"
|
||||
val ShieldGenerator = "shield_generator"
|
||||
val Shifter = "shifter"
|
||||
val SkyDome = "skydome"
|
||||
val SpawnPlayer = "spawn_player"
|
||||
val SpawnPoint = "spawn_point"
|
||||
val SpawnTerminal = "spawn_terminal"
|
||||
val TeleportPad = "teleport_pad"
|
||||
val Terminal = "terminal"
|
||||
val TradeContainer = "trade_container"
|
||||
val UplinkDevice = "uplink_device"
|
||||
val VanuCradleClass = "vanu_cradle_class"
|
||||
val VanuModuleClass = "vanu_module_class"
|
||||
val VanuModuleFactory = "vanu_module_factory"
|
||||
val VanuReceptacleClass = "vanu_receptacle_class"
|
||||
val Vehicle = "vehicle"
|
||||
val VehicleCreationPad = "vehicle_creation_pad"
|
||||
val VehicleLandingPad = "vehicle_landing_pad"
|
||||
val VehicleTerminal = "vehicle_terminal"
|
||||
val Warpgate = "waprgate"
|
||||
val WarpZone = "warp_zone"
|
||||
val Weapon = "weapon"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
65
src/main/scala/net/psforever/objects/OwnableByPlayer.scala
Normal file
65
src/main/scala/net/psforever/objects/OwnableByPlayer.scala
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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})"
|
||||
}
|
||||
}
|
||||
537
src/main/scala/net/psforever/objects/Player.scala
Normal file
537
src/main/scala/net/psforever/objects/Player.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/main/scala/net/psforever/objects/Players.scala
Normal file
137
src/main/scala/net/psforever/objects/Players.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/main/scala/net/psforever/objects/SensorDeployable.scala
Normal file
151
src/main/scala/net/psforever/objects/SensorDeployable.scala
Normal 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)?
|
||||
}
|
||||
}
|
||||
17
src/main/scala/net/psforever/objects/Session.scala
Normal file
17
src/main/scala/net/psforever/objects/Session.scala
Normal 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
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
15
src/main/scala/net/psforever/objects/SimpleItem.scala
Normal file
15
src/main/scala/net/psforever/objects/SimpleItem.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
171
src/main/scala/net/psforever/objects/SpawnPoint.scala
Normal file
171
src/main/scala/net/psforever/objects/SpawnPoint.scala
Normal 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)
|
||||
}
|
||||
13
src/main/scala/net/psforever/objects/Telepad.scala
Normal file
13
src/main/scala/net/psforever/objects/Telepad.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
241
src/main/scala/net/psforever/objects/Tool.scala
Normal file
241
src/main/scala/net/psforever/objects/Tool.scala
Normal 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
|
||||
}
|
||||
}
|
||||
50
src/main/scala/net/psforever/objects/TrapDeployable.scala
Normal file
50
src/main/scala/net/psforever/objects/TrapDeployable.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
93
src/main/scala/net/psforever/objects/TurretDeployable.scala
Normal file
93
src/main/scala/net/psforever/objects/TurretDeployable.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
698
src/main/scala/net/psforever/objects/Vehicle.scala
Normal file
698
src/main/scala/net/psforever/objects/Vehicle.scala
Normal 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)"
|
||||
}
|
||||
}
|
||||
411
src/main/scala/net/psforever/objects/Vehicles.scala
Normal file
411
src/main/scala/net/psforever/objects/Vehicles.scala
Normal 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 _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/main/scala/net/psforever/objects/avatar/Avatar.scala
Normal file
188
src/main/scala/net/psforever/objects/avatar/Avatar.scala
Normal 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)
|
||||
}
|
||||
19
src/main/scala/net/psforever/objects/avatar/Avatars.scala
Normal file
19
src/main/scala/net/psforever/objects/avatar/Avatars.scala
Normal 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)
|
||||
}
|
||||
106
src/main/scala/net/psforever/objects/avatar/BattleRank.scala
Normal file
106
src/main/scala/net/psforever/objects/avatar/BattleRank.scala
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
219
src/main/scala/net/psforever/objects/avatar/Certification.scala
Normal file
219
src/main/scala/net/psforever/objects/avatar/Certification.scala
Normal 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)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
66
src/main/scala/net/psforever/objects/avatar/Cosmetic.scala
Normal file
66
src/main/scala/net/psforever/objects/avatar/Cosmetic.scala
Normal 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))
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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 _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
16
src/main/scala/net/psforever/objects/avatar/Implant.scala
Normal file
16
src/main/scala/net/psforever/objects/avatar/Implant.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
874
src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
Normal file
874
src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
126
src/main/scala/net/psforever/objects/ballistics/Projectile.scala
Normal file
126
src/main/scala/net/psforever/objects/ballistics/Projectile.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(" ")
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
110
src/main/scala/net/psforever/objects/ce/Deployable.scala
Normal file
110
src/main/scala/net/psforever/objects/ce/Deployable.scala
Normal 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
22
src/main/scala/net/psforever/objects/ce/DeployedItem.scala
Normal file
22
src/main/scala/net/psforever/objects/ce/DeployedItem.scala
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
102
src/main/scala/net/psforever/objects/ce/TelepadLike.scala
Normal file
102
src/main/scala/net/psforever/objects/ce/TelepadLike.scala
Normal 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 _ => ;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue