mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-24 09:03:35 +00:00
Restructure repository
* Move /common/src to /src * Move services to net.psforever package * Move /pslogin to /server
This commit is contained in:
parent
89a30ae6f6
commit
f4fd78fc5d
958 changed files with 527 additions and 725 deletions
|
|
@ -1,13 +0,0 @@
|
|||
package scodec.interop.akka;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import akka.util.ByteString.ByteString1C;
|
||||
|
||||
interface PrivacyHelper {
|
||||
|
||||
static ByteString1C createByteString1C(byte[] array) {
|
||||
return new ByteString1C(array);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
# The socket bind address for all 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"
|
||||
|
|
@ -1,506 +0,0 @@
|
|||
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
|
|
@ -1,66 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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
|
|
@ -1,934 +0,0 @@
|
|||
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 services.chat.ChatService
|
||||
import 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
|
|
@ -1,175 +0,0 @@
|
|||
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 services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||
import services.local.{LocalAction, LocalServiceMessage}
|
||||
import 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,320 +0,0 @@
|
|||
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 services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
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 services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,650 +0,0 @@
|
|||
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 services.Service
|
||||
import 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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 =>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
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 services.ServiceManager.Lookup
|
||||
import 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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 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 services
|
||||
case class CommandInternal(usage: String, handler: ((Array[String]) => CommandResponse)) extends CommandInfo
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// 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)"
|
||||
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// 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)"
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// 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)")
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package net.psforever.objects
|
||||
|
||||
case class Account(
|
||||
id: Int,
|
||||
name: String,
|
||||
gm: Boolean = false
|
||||
)
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// 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})"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
// 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 services.RemoverActor
|
||||
import 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
// 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 services.Service
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import 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
|
|
@ -1,19 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
|
||||
import scala.collection.concurrent.{Map, TrieMap}
|
||||
|
||||
/**
|
||||
* See the companion object for class and method documentation.
|
||||
* `LivePlayerList` is a singleton and this private class lacks exposure.
|
||||
*/
|
||||
private class LivePlayerList {
|
||||
|
||||
/** key - the session id; value - a `Player` object */
|
||||
private val sessionMap: Map[Long, Avatar] = new TrieMap[Long, Avatar]
|
||||
|
||||
def WorldPopulation(predicate: ((_, Avatar)) => Boolean): List[Avatar] = {
|
||||
sessionMap.filter(predicate).values.toList
|
||||
}
|
||||
|
||||
def Add(sessionId: Long, avatar: Avatar): Boolean = {
|
||||
sessionMap.values.find(char => char.equals(avatar)) match {
|
||||
case None =>
|
||||
sessionMap.putIfAbsent(sessionId, avatar).isEmpty
|
||||
case Some(_) =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def Update(sessionId: Long, avatar: Avatar): Unit = {
|
||||
sessionMap.get(sessionId) match {
|
||||
case Some(_) =>
|
||||
sessionMap(sessionId) = avatar
|
||||
case None => ;
|
||||
}
|
||||
}
|
||||
|
||||
def Remove(sessionId: Long): Option[Avatar] = {
|
||||
sessionMap.remove(sessionId)
|
||||
}
|
||||
|
||||
def Shutdown: List[Avatar] = {
|
||||
val list = sessionMap.values.toList
|
||||
sessionMap.clear()
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for storing `Player` mappings for users that are currently online.
|
||||
* The mapping system is tightly coupled between the `Avatar` class and to an instance of `WorldSessionActor`.
|
||||
* <br>
|
||||
* Use:<br>
|
||||
* 1) When a users logs in during `WorldSessionActor`, associate that user's session id and their character (avatar).<br>
|
||||
* `LivePlayerList.Add(session, avatar)`<br>
|
||||
* 2) In between the previous two steps, a range of characters may be queried based on provided statistics.<br>
|
||||
* `LivePlayerList.WorldPopulation(...)`<br>
|
||||
* 3) When the user leaves the game entirely, his character's entry is removed from the mapping.<br>
|
||||
* `LivePlayerList.Remove(session)`
|
||||
*/
|
||||
object LivePlayerList {
|
||||
|
||||
/** As `LivePlayerList` is a singleton, an object of `LivePlayerList` is automatically instantiated. */
|
||||
private val Instance: LivePlayerList = new LivePlayerList
|
||||
|
||||
/**
|
||||
* Given some criteria, examine the mapping of user characters and find the ones that fulfill the requirements.<br>
|
||||
* <br>
|
||||
* Note the signature carefully.
|
||||
* A two-element tuple is checked, but only the second element of that tuple - a `Player` - is eligible for being queried.
|
||||
* The first element is ignored.
|
||||
* Even a predicate as simple as `{ case ((x : Long, _)) => x > 0 }` will not work for that reason.
|
||||
* @param predicate the conditions for filtering the live `Player`s
|
||||
* @return a list of users's `Player`s that fit the criteria
|
||||
*/
|
||||
def WorldPopulation(predicate: ((_, Avatar)) => Boolean): List[Avatar] = Instance.WorldPopulation(predicate)
|
||||
|
||||
/**
|
||||
* Create a mapped entry between the user's session and a user's character.
|
||||
* Neither the player nor the session may exist in the current mappings if this is to work.
|
||||
*
|
||||
* @param sessionId the session
|
||||
* @param avatar the character
|
||||
* @return `true`, if the session was association was made; `false`, otherwise
|
||||
*/
|
||||
def Add(sessionId: Long, avatar: Avatar): Boolean = Instance.Add(sessionId, avatar)
|
||||
|
||||
def Update(sessionId: Long, avatar: Avatar): Unit = Instance.Update(sessionId, avatar)
|
||||
|
||||
/**
|
||||
* Remove all entries related to the given session identifier from the mappings.
|
||||
* The character no longer counts as "online."
|
||||
*
|
||||
* @param sessionId the session
|
||||
* @return any character that was afffected by the mapping removal
|
||||
*/
|
||||
def Remove(sessionId: Long): Option[Avatar] = Instance.Remove(sessionId)
|
||||
|
||||
/**
|
||||
* Hastily remove all mappings and ids.
|
||||
*
|
||||
* @return an unsorted list of the characters that were still online
|
||||
*/
|
||||
def Shutdown: List[Avatar] = Instance.Shutdown
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
// 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 services.Service
|
||||
import 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
// 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"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
// 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})"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,537 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
// 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 services.Service
|
||||
import 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
// 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 services.Service
|
||||
import services.local.{LocalAction, LocalServiceMessage}
|
||||
import 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)?
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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
|
||||
)
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
// 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 services.Service
|
||||
import 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// 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
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,698 +0,0 @@
|
|||
// 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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,410 +0,0 @@
|
|||
// 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 services.{RemoverActor, Service}
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import services.local.{LocalAction, LocalServiceMessage}
|
||||
import 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 _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// 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 services.Service
|
||||
import 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
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))
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -1,714 +0,0 @@
|
|||
// 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 _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,466 +0,0 @@
|
|||
// 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"
|
||||
)
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1,874 +0,0 @@
|
|||
// 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 services.{RemoverActor, Service}
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import 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
Loading…
Add table
Add a link
Reference in a new issue