Restructure repository

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

View file

@ -0,0 +1,13 @@
package scodec.interop.akka;
import java.nio.ByteBuffer;
import akka.util.ByteString.ByteString1C;
interface PrivacyHelper {
static ByteString1C createByteString1C(byte[] array) {
return new ByteString1C(array);
}
}

View file

@ -0,0 +1,252 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = INFO
logging-filter = akka.event.slf4j.Slf4jLoggingFilter
}
akka.actor.deployment {
"/login-udp-endpoint" {
dispatcher = network-listener
}
"/login-udp-endpoint/login-session-router" {
dispatcher = login-session-router
}
"/login-udp-endpoint/login-session-router/*" {
dispatcher = login-session
}
# Extremely performance critical (dedicated thread)
"/world-udp-endpoint" {
dispatcher = network-listener
}
# Extremely performance critical (dedicated thread)
"/world-udp-endpoint/world-session-router" {
dispatcher = world-session-router
}
# Most likely to crash (isolate)
"/world-udp-endpoint/world-session-router/*" {
dispatcher = world-session
}
# Service dedicated pool
"/service" {
dispatcher = service-dispatcher
}
"/service/*" {
dispatcher = service-dispatcher
}
# Bottleneck (dedicated thread)
"/service/galaxy" {
dispatcher = galaxy-service
}
# Isolate tasks
"/service/taskResolver*" {
dispatcher = task-dispatcher
}
# Bottleneck (dedicated thread)
"/service/cluster" {
dispatcher = interstellar-cluster-service
}
# Zone actors (lots of messages)
"/service/cluster/c1-actor" {
dispatcher = c1-zone-dispatcher
}
"/service/cluster/c1-actor/*" {
dispatcher = c1-zone-dispatcher
}
"/service/cluster/c2-actor" {
dispatcher = c2-zone-dispatcher
}
"/service/cluster/c2-actor/*" {
dispatcher = c2-zone-dispatcher
}
"/service/cluster/c3-actor" {
dispatcher = c3-zone-dispatcher
}
"/service/cluster/c3-actor/*" {
dispatcher = c3-zone-dispatcher
}
"/service/cluster/c4-actor" {
dispatcher = c4-zone-dispatcher
}
"/service/cluster/c4-actor/*" {
dispatcher = c4-zone-dispatcher
}
"/service/cluster/c5-actor" {
dispatcher = c5-zone-dispatcher
}
"/service/cluster/c5-actor/*" {
dispatcher = c5-zone-dispatcher
}
"/service/cluster/c6-actor" {
dispatcher = c6-zone-dispatcher
}
"/service/cluster/c6-actor/*" {
dispatcher = c6-zone-dispatcher
}
"/service/cluster/i1-actor" {
dispatcher = i1-zone-dispatcher
}
"/service/cluster/i1-actor/*" {
dispatcher = i1-zone-dispatcher
}
"/service/cluster/i2-actor" {
dispatcher = i2-zone-dispatcher
}
"/service/cluster/i2-actor/*" {
dispatcher = i2-zone-dispatcher
}
"/service/cluster/i3-actor" {
dispatcher = i3-zone-dispatcher
}
"/service/cluster/i3-actor/*" {
dispatcher = i3-zone-dispatcher
}
"/service/cluster/i4-actor" {
dispatcher = i4-zone-dispatcher
}
"/service/cluster/i4-actor/*" {
dispatcher = i4-zone-dispatcher
}
"/service/cluster/z1-actor" {
dispatcher = z1-zone-dispatcher
}
"/service/cluster/z1-actor/*" {
dispatcher = z1-zone-dispatcher
}
"/service/cluster/z2-actor" {
dispatcher = z2-zone-dispatcher
}
"/service/cluster/z2-actor/*" {
dispatcher = z2-zone-dispatcher
}
"/service/cluster/z3-actor" {
dispatcher = z3-zone-dispatcher
}
"/service/cluster/z3-actor/*" {
dispatcher = z3-zone-dispatcher
}
"/service/cluster/z4-actor" {
dispatcher = z4-zone-dispatcher
}
"/service/cluster/z4-actor/*" {
dispatcher = z4-zone-dispatcher
}
"/service/cluster/z5-actor" {
dispatcher = z5-zone-dispatcher
}
"/service/cluster/z5-actor/*" {
dispatcher = z5-zone-dispatcher
}
"/service/cluster/z6-actor" {
dispatcher = z6-zone-dispatcher
}
"/service/cluster/z6-actor/*" {
dispatcher = z6-zone-dispatcher
}
"/service/cluster/z7-actor" {
dispatcher = z7-zone-dispatcher
}
"/service/cluster/z7-actor/*" {
dispatcher = z7-zone-dispatcher
}
"/service/cluster/z8-actor" {
dispatcher = z8-zone-dispatcher
}
"/service/cluster/z8-actor/*" {
dispatcher = z8-zone-dispatcher
}
"/service/cluster/z9-actor" {
dispatcher = z9-zone-dispatcher
}
"/service/cluster/z9-actor/*" {
dispatcher = z9-zone-dispatcher
}
"/service/cluster/z10-actor" {
dispatcher = z10-zone-dispatcher
}
"/service/cluster/z10-actor/*" {
dispatcher = z10-zone-dispatcher
}
"/service/cluster/home1-actor" {
dispatcher = home1-zone-dispatcher
}
"/service/cluster/home1-actor/*" {
dispatcher = home1-zone-dispatcher
}
"/service/cluster/home2-actor" {
dispatcher = home2-zone-dispatcher
}
"/service/cluster/home2-actor/*" {
dispatcher = home2-zone-dispatcher
}
"/service/cluster/home3-actor" {
dispatcher = home3-zone-dispatcher
}
"/service/cluster/home3-actor/*" {
dispatcher = home3-zone-dispatcher
}
"/service/cluster/tzconc-actor" {
dispatcher = tzconc-zone-dispatcher
}
"/service/cluster/tzconc-actor/*" {
dispatcher = tzconc-zone-dispatcher
}
"/service/cluster/tzcotr-actor" {
dispatcher = tzcotr-zone-dispatcher
}
"/service/cluster/tzcotr-actor/*" {
dispatcher = tzcotr-zone-dispatcher
}
"/service/cluster/tzcovs-actor" {
dispatcher = tzcovs-zone-dispatcher
}
"/service/cluster/tzcovs-actor/*" {
dispatcher = tzcovs-zone-dispatcher
}
"/service/cluster/tzdrnc-actor" {
dispatcher = tzdrnc-zone-dispatcher
}
"/service/cluster/tzdrnc-actor/*" {
dispatcher = tzdrnc-zone-dispatcher
}
"/service/cluster/tzdrvs-actor" {
dispatcher = tzdrvs-zone-dispatcher
}
"/service/cluster/tzdrvs-actor/*" {
dispatcher = tzdrvs-zone-dispatcher
}
"/service/cluster/tzsdrtr-actor" {
dispatcher = tzsdrtr-zone-dispatcher
}
"/service/cluster/tzsdrtr-actor/*" {
dispatcher = tzsdrtr-zone-dispatcher
}
"/service/cluster/tzshnc-actor" {
dispatcher = tzshnc-zone-dispatcher
}
"/service/cluster/tzshnc-actor/*" {
dispatcher = tzshnc-zone-dispatcher
}
"/service/cluster/tzshtr-actor" {
dispatcher = tzshtr-zone-dispatcher
}
"/service/cluster/tzshtr-actor/*" {
dispatcher = tzshtr-zone-dispatcher
}
"/service/cluster/tzshvs-actor" {
dispatcher = tzshvs-zone-dispatcher
}
"/service/cluster/tzshvs-actor/*" {
dispatcher = tzshvs-zone-dispatcher
}
}

View file

@ -0,0 +1,131 @@
# The socket bind address for all net.psforever.services except admin. 127.0.0.1 is the
# default for local testing, for public servers use 0.0.0.0 instead.
bind = 127.0.0.1
# The public host name or IP address. Used to forward clients from the login
# server to the world server. The default value will only allow local connections.
public = 127.0.0.1
# Login server configuration
login {
# UDP listening port
port = 51000
# Account usernames that don't exist yet will be automatically created in the
# database. Useful for test servers and development testing.
create-missing-accounts = yes
}
# World server configuration
world {
# UDP listening port
port = 51001
# The name of the server as displayed in the server browser.
server-name = PSForever
# How the server is displayed in the server browser.
# One of: released beta development
server-type = released
}
# Admin API configuration
admin {
# TCP listening port
port = 51002
# The socket bind address
bind = 127.0.0.1
}
# Database configuration
database {
# The hostname of the PostgreSQL server.
host = localhost
# The TCP port to connect to the database with.
port = 5432
# The username to connect to the SQL server with.
username = psforever
# The password to connect to the SQL server with.
password = psforever
# The database name to use on the SQL server.
database = psforever
# The SSL configuration of the database connection.
# One of: disable prefer require verify-full
sslmode = prefer
# The maximum number of active connections.
maxActiveConnections = 5
}
# Enable non-standard game properties
game {
# Allow instant action to AMS
instant-action-ams = no
}
anti-cheat {
# The distance (squared) threshold that triggers if the reported hit location
# of a shot does not match the object being hit's location on the server
# One of 1-1000000 (sqrt 10000 = ~100 ingame units)
hit-position-discrepancy-threshold = 10000
}
network {
session {
# The maximum amount of time since the last inbound packet from a UDP session
# before it is dropped.
inbound-grace-time = 1 minute
# The maximum amount of time since the last outbound packet for a UDP session
# before it is dropped. Can be used as a watchdog for hung server sessions.
outbound-grace-time = 1 minute
}
}
developer {
net-sim {
# Enable artificial packet unreliability. Used for development testing.
# Active equally on upstream and downstream packets.
enable = no
# The percentage of outgoing and incoming packets that are dropped.
loss = 0.02
# The time a packet is buffered before being delivered to simulate delay.
# The artificial delay is in addition to any real network latency.
delay = 150 milliseconds
# The percentage chance that a packet will be ordered randomly in the delay
# buffer. If the delay is too small then packets won't be reordered.
reorder-chance = 0.005
# If a packet is reordered, the maximum time in the future or the past where
# it will randomly appear.
reorder-time = 150 milliseconds
}
}
kamon {
# Enables reporting of metrics to Kamon.io
enable = no
environment.service = "PSForever"
apm.api-key = ""
}
sentry {
# Enables submission of warnings and errors to Sentry
enable = no
# Sentry DSN (Data Source Name)
dsn = ""
}
include "akka.conf"
include "dispatchers.conf"

View file

@ -0,0 +1,506 @@
login-session {
# Dispatcher is the name of the event-based dispatcher
type = Dispatcher
# What kind of ExecutionService to use
executor = "fork-join-executor"
# Configuration for the fork join pool
fork-join-executor {
# Min number of threads to cap factor-based parallelism number to
parallelism-min = 8
# Parallelism (threads) ... ceil(available processors * factor)
parallelism-factor = 2.0
# Max number of threads to cap factor-based parallelism number to
parallelism-max = 64
}
# Throughput defines the maximum number of messages to be
# processed per actor before the thread jumps to the next actor.
# Set to 1 for as fair as possible.
throughput = 50
throughput-deadline-time = 20ms
}
world-session {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 8
parallelism-factor = 2.0
parallelism-max = 64
}
throughput = 50
throughput-deadline-time = 50ms
}
login-session-router {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
world-session-router {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
network-listener {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
interstellar-cluster-service {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
galaxy-service {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
task-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 2
parallelism-factor = 2.0
parallelism-max = 64
}
throughput = 50
throughput-deadline-time = 50ms
}
service-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 2
parallelism-factor = 2.0
parallelism-max = 64
}
throughput = 50
throughput-deadline-time = 50ms
}
c1-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
c2-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
c3-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
c4-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
c5-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
c6-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
i1-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
i2-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
i3-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
i4-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z1-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z2-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z3-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z4-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z5-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z6-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z7-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z8-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z9-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
z10-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
home1-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
home2-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
home3-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzconc-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzcotr-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzcovs-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzdrnc-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzdrvs-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzsdrtr-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzshnc-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzshtr-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}
tzshvs-zone-dispatcher {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 1
parallelism-factor = 2.0
parallelism-max = 4
}
throughput = 50
throughput-deadline-time = 50ms
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,96 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
object ObjectType extends Enumeration {
type Value = String
val AmbientSoundSource = "ambient_sound_source"
val Ammunition = "ammunition"
val AnimatedBarrier = "animated_barrier"
val Applicator = "applicator"
val Armor = "armor"
val ArmorSiphon = "armor_siphon"
val AwardStatistic = "award_statistic"
val Avatar = "avatar"
val AvatarBot = "avatar_bot"
val Ball = "ball"
val Bank = "bank"
val Barrier = "barrier"
val BfrTerminal = "bfr_terminal"
val Billboard = "billboard"
val Boomer = "boomer"
val BoomerTrigger = "boomer_trigger"
val Building = "building"
val CaptureFlag = "capture_flag"
val CaptureFlagSocket = "capture_flag_socket"
val CaptureTerminal = "capture_terminal"
val CertTerminal = "cert_terminal"
val ChainLashDamager = "chain_lash_damager"
val Dispenser = "dispenser"
val Door = "door"
val EmpBlast = "emp_blast"
val FrameVehicle = "framevehicle"
val Flag = "flag"
val FlightVehicle = "flightvehicle"
val ForceDome = "forcedome"
val ForceDomeGenerator = "forcedomegenerator"
val Game = "game"
val Generic = "generic"
val GenericTeleportion = "generic_teleportation"
val GeneratorTerminal = "generator_terminal"
val GsGenbase = "GS_genbase"
val HandGrenade = "hand_grenade"
val HeMine = "he_mine"
val HeavyWeapon = "heavy_weapon"
val HoverVehicle = "hovervehicle"
val Implant = "implant"
val ImplantInterfaceTerminal = "implant_terminal_interface"
val Lazer = "lazer"
val Locker = "locker"
val LockerContainer = "locker_container"
val LockExternal = "lock_external"
val LockSmall = "lock_small"
val MainTerminal = "main_terminal"
val Map = "map"
val MedicalTerminal = "medical_terminal"
val Medkit = "medkit"
val Monolith = "monolith"
val MonolithUnit = "monolith_unit"
val MotionAlarmSensorDest = "motion_alarm_sensor_dest"
val NanoDispenser = "nano_dispenser"
val NtuSipon = "ntu_siphon"
val OrbitalShuttlePad = "orbital_shuttle_pad"
val OrbitalStrike = "orbital_strike"
val OrderTerminal = "order_terminal"
val PainTerminal = "pain_terminal"
val Projectile = "projectile"
val RadiationCloud = "radiation_cloud"
val RearmTerminal = "rearm_terminal"
val RechargeTerminal = "recharge_terminal"
val Rek = "rek"
val RepairTerminal = "repair_terminal"
val ResourceSilo = "resource_silo"
val RespawnTube = "respawn_tube"
val SensorShield = "sensor_shield"
val ShieldGenerator = "shield_generator"
val Shifter = "shifter"
val SkyDome = "skydome"
val SpawnPlayer = "spawn_player"
val SpawnPoint = "spawn_point"
val SpawnTerminal = "spawn_terminal"
val TeleportPad = "teleport_pad"
val Terminal = "terminal"
val TradeContainer = "trade_container"
val UplinkDevice = "uplink_device"
val VanuCradleClass = "vanu_cradle_class"
val VanuModuleClass = "vanu_module_class"
val VanuModuleFactory = "vanu_module_factory"
val VanuReceptacleClass = "vanu_receptacle_class"
val Vehicle = "vehicle"
val VehicleCreationPad = "vehicle_creation_pad"
val VehicleLandingPad = "vehicle_landing_pad"
val VehicleTerminal = "vehicle_terminal"
val Warpgate = "waprgate"
val WarpZone = "warp_zone"
val Weapon = "weapon"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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