mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
Merge branch 'master' into reduce-psm-load
This commit is contained in:
commit
61a75f13cd
106 changed files with 468298 additions and 468246 deletions
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||||
43
.devcontainer/devcontainer.json
Normal file
43
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/java-postgres
|
||||||
|
{
|
||||||
|
"name": "Java & PostgreSQL",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/sbt-sdkman:2": {
|
||||||
|
"jdkVersion": "11"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/scala-sdkman:2": {
|
||||||
|
"jdkVersion": "11"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/scalacli-sdkman:2": {
|
||||||
|
"jdkVersion": "11"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {}
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// This can be used to network with other containers or with the host.
|
||||||
|
// "forwardPorts": [
|
||||||
|
// 51000,
|
||||||
|
// 51001,
|
||||||
|
// 51002
|
||||||
|
// ],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"scalameta.metals",
|
||||||
|
"scala-lang.scala",
|
||||||
|
"EditorConfig.EditorConfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "java -version",
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
31
.devcontainer/docker-compose.yml
Normal file
31
.devcontainer/docker-compose.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: javadev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
environment:
|
||||||
|
CONFIG_FORCE_database_host: postgres
|
||||||
|
CONFIG_FORCE_bind: 0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- ../..:/workspaces:cached
|
||||||
|
command: sleep infinity
|
||||||
|
# network_mode: service:postgres
|
||||||
|
ports:
|
||||||
|
- "51000:51000/udp"
|
||||||
|
- "51001:51001/udp"
|
||||||
|
# - "51000:51002"
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: psforever
|
||||||
|
POSTGRES_USER: psforever
|
||||||
|
POSTGRES_DB: psforever
|
||||||
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
1
.jvmopts
1
.jvmopts
|
|
@ -2,3 +2,4 @@
|
||||||
-Xss6M
|
-Xss6M
|
||||||
-Dconfig.override_with_env_vars=true
|
-Dconfig.override_with_env_vars=true
|
||||||
-Dsbt.server.forcestart=true
|
-Dsbt.server.forcestart=true
|
||||||
|
-Dquill.macro.log=false
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mozilla/sbt as builder
|
FROM sbtscala/scala-sbt:eclipse-temurin-focal-11.0.17_8_1.8.2_2.13.10 as builder
|
||||||
|
|
||||||
COPY . /PSF-LoginServer
|
COPY . /PSF-LoginServer
|
||||||
|
|
||||||
|
|
|
||||||
55
build.sbt
55
build.sbt
|
|
@ -3,7 +3,9 @@ import xerial.sbt.pack.PackPlugin._
|
||||||
lazy val psforeverSettings = Seq(
|
lazy val psforeverSettings = Seq(
|
||||||
organization := "net.psforever",
|
organization := "net.psforever",
|
||||||
version := "1.0.2-SNAPSHOT",
|
version := "1.0.2-SNAPSHOT",
|
||||||
scalaVersion := "2.13.3",
|
// TODO 2.13.5+ breaks Md5Mac test
|
||||||
|
// possibly due to ListBuffer changes? https://github.com/scala/scala/pull/9257
|
||||||
|
scalaVersion := "2.13.4",
|
||||||
Global / cancelable := false,
|
Global / cancelable := false,
|
||||||
semanticdbEnabled := true,
|
semanticdbEnabled := true,
|
||||||
semanticdbVersion := scalafixSemanticdb.revision,
|
semanticdbVersion := scalafixSemanticdb.revision,
|
||||||
|
|
@ -40,53 +42,50 @@ lazy val psforeverSettings = Seq(
|
||||||
classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat,
|
classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat,
|
||||||
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
|
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"com.typesafe.akka" %% "akka-actor" % "2.6.17",
|
"com.typesafe.akka" %% "akka-actor" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.17",
|
"com.typesafe.akka" %% "akka-slf4j" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.17",
|
"com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-stream" % "2.6.17",
|
"com.typesafe.akka" %% "akka-stream" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-testkit" % "2.6.17" % "test",
|
"com.typesafe.akka" %% "akka-testkit" % "2.6.20" % "test",
|
||||||
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.17",
|
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-actor-testkit-typed" % "2.6.17" % "test",
|
"com.typesafe.akka" %% "akka-actor-testkit-typed" % "2.6.20" % "test",
|
||||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.17",
|
"com.typesafe.akka" %% "akka-slf4j" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.17",
|
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-coordination" % "2.6.17",
|
"com.typesafe.akka" %% "akka-coordination" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-cluster-tools" % "2.6.17",
|
"com.typesafe.akka" %% "akka-cluster-tools" % "2.6.20",
|
||||||
"com.typesafe.akka" %% "akka-http" % "10.2.6",
|
"com.typesafe.akka" %% "akka-http" % "10.2.6",
|
||||||
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.4",
|
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.4",
|
||||||
"org.specs2" %% "specs2-core" % "4.13.0" % "test",
|
"org.specs2" %% "specs2-core" % "4.20.0" % "test",
|
||||||
"org.scalatest" %% "scalatest" % "3.2.10" % "test",
|
"org.scalatest" %% "scalatest" % "3.2.15" % "test",
|
||||||
"org.scodec" %% "scodec-core" % "1.11.9",
|
"org.scodec" %% "scodec-core" % "1.11.9",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.2.6",
|
"ch.qos.logback" % "logback-classic" % "1.2.6",
|
||||||
"org.log4s" %% "log4s" % "1.10.0",
|
"org.log4s" %% "log4s" % "1.10.0",
|
||||||
"org.fusesource.jansi" % "jansi" % "2.4.0",
|
"org.fusesource.jansi" % "jansi" % "2.4.0",
|
||||||
"org.scoverage" %% "scalac-scoverage-plugin" % "1.4.2",
|
|
||||||
"com.github.nscala-time" %% "nscala-time" % "2.30.0",
|
"com.github.nscala-time" %% "nscala-time" % "2.30.0",
|
||||||
"com.github.t3hnar" %% "scala-bcrypt" % "4.3.0",
|
"com.github.t3hnar" %% "scala-bcrypt" % "4.3.0",
|
||||||
"org.scala-graph" %% "graph-core" % "1.13.3",
|
"org.scala-graph" %% "graph-core" % "1.13.3",
|
||||||
"io.kamon" %% "kamon-bundle" % "2.3.1",
|
|
||||||
"io.kamon" %% "kamon-apm-reporter" % "2.3.1",
|
|
||||||
"org.json4s" %% "json4s-native" % "4.0.3",
|
"org.json4s" %% "json4s-native" % "4.0.3",
|
||||||
"io.getquill" %% "quill-jasync-postgres" % "3.12.0",
|
"io.getquill" %% "quill-jasync-postgres" % "3.18.0",
|
||||||
"org.flywaydb" % "flyway-core" % "8.0.3",
|
"org.flywaydb" % "flyway-core" % "9.0.0",
|
||||||
"org.postgresql" % "postgresql" % "42.3.1",
|
"org.postgresql" % "postgresql" % "42.3.1",
|
||||||
"com.typesafe" % "config" % "1.4.1",
|
"com.typesafe" % "config" % "1.4.1",
|
||||||
"com.github.pureconfig" %% "pureconfig" % "0.17.0",
|
"com.github.pureconfig" %% "pureconfig" % "0.17.0",
|
||||||
"com.beachape" %% "enumeratum" % "1.7.0",
|
"com.beachape" %% "enumeratum" % "1.7.0",
|
||||||
"joda-time" % "joda-time" % "2.10.13",
|
|
||||||
"commons-io" % "commons-io" % "2.11.0",
|
"commons-io" % "commons-io" % "2.11.0",
|
||||||
"com.github.scopt" %% "scopt" % "4.0.1",
|
"com.github.scopt" %% "scopt" % "4.1.0",
|
||||||
"io.sentry" % "sentry-logback" % "5.3.0",
|
"io.sentry" % "sentry-logback" % "6.16.0",
|
||||||
"io.circe" %% "circe-core" % "0.14.1",
|
"io.circe" %% "circe-core" % "0.14.5",
|
||||||
"io.circe" %% "circe-generic" % "0.14.1",
|
"io.circe" %% "circe-generic" % "0.14.5",
|
||||||
"io.circe" %% "circe-parser" % "0.14.1",
|
"io.circe" %% "circe-parser" % "0.14.5",
|
||||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4",
|
"org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4",
|
||||||
"org.bouncycastle" % "bcprov-jdk15on" % "1.69"
|
"org.bouncycastle" % "bcprov-jdk15on" % "1.69"
|
||||||
),
|
),
|
||||||
dependencyOverrides ++= Seq(
|
dependencyOverrides ++= Seq(
|
||||||
"com.github.jasync-sql" % "jasync-postgresql" % "1.1.7"
|
"com.github.jasync-sql" % "jasync-postgresql" % "1.1.7",
|
||||||
),
|
"org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2"
|
||||||
|
)
|
||||||
// TODO(chord): remove exclusion when SessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279
|
// TODO(chord): remove exclusion when SessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279
|
||||||
coverageExcludedPackages := "net\\.psforever\\.actors\\.session\\.SessionActor.*"
|
// coverageExcludedPackages := "net\\.psforever\\.actors\\.session\\.SessionActor.*"
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val psforever = (project in file("."))
|
lazy val psforever = (project in file("."))
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
sbt.version = 1.4.5
|
sbt.version = 1.8.2
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
logLevel := Level.Warn
|
logLevel := Level.Warn
|
||||||
|
|
||||||
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.14")
|
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.17")
|
||||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.1")
|
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7")
|
||||||
addSbtPlugin("io.kamon" % "sbt-kanela-runner" % "2.0.12")
|
addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3")
|
||||||
addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3")
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
|
||||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
|
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4")
|
||||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.31")
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import java.nio.file.Paths
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.UUID.randomUUID
|
import java.util.UUID.randomUUID
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.concurrent.Await
|
||||||
|
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.ActorSystem
|
||||||
import akka.actor.typed.ActorRef
|
import akka.actor.typed.ActorRef
|
||||||
|
|
@ -13,7 +15,6 @@ import akka.{actor => classic}
|
||||||
import ch.qos.logback.classic.LoggerContext
|
import ch.qos.logback.classic.LoggerContext
|
||||||
import ch.qos.logback.classic.joran.JoranConfigurator
|
import ch.qos.logback.classic.joran.JoranConfigurator
|
||||||
import io.sentry.{Sentry, SentryOptions}
|
import io.sentry.{Sentry, SentryOptions}
|
||||||
import kamon.Kamon
|
|
||||||
import net.psforever.actors.net.{LoginActor, MiddlewareActor, SocketActor}
|
import net.psforever.actors.net.{LoginActor, MiddlewareActor, SocketActor}
|
||||||
import net.psforever.actors.session.SessionActor
|
import net.psforever.actors.session.SessionActor
|
||||||
import net.psforever.login.psadmin.PsAdminActor
|
import net.psforever.login.psadmin.PsAdminActor
|
||||||
|
|
@ -37,6 +38,7 @@ import scopt.OParser
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import net.psforever.packet.PlanetSidePacket
|
import net.psforever.packet.PlanetSidePacket
|
||||||
import net.psforever.services.hart.HartService
|
import net.psforever.services.hart.HartService
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
|
|
||||||
object Server {
|
object Server {
|
||||||
private val logger = org.log4s.getLogger
|
private val logger = org.log4s.getLogger
|
||||||
|
|
@ -80,11 +82,6 @@ object Server {
|
||||||
case None => InetAddress.getByName(Config.app.bind) // address from config
|
case None => InetAddress.getByName(Config.app.bind) // address from config
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.app.kamon.enable) {
|
|
||||||
logger.info("Starting Kamon")
|
|
||||||
Kamon.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config.app.sentry.enable) {
|
if (Config.app.sentry.enable) {
|
||||||
logger.info(s"Enabling Sentry")
|
logger.info(s"Enabling Sentry")
|
||||||
val options = new SentryOptions()
|
val options = new SentryOptions()
|
||||||
|
|
@ -110,8 +107,9 @@ object Server {
|
||||||
}
|
}
|
||||||
val session = (ref: ActorRef[MiddlewareActor.Command], info: InetSocketAddress, connectionId: String) => {
|
val session = (ref: ActorRef[MiddlewareActor.Command], info: InetSocketAddress, connectionId: String) => {
|
||||||
Behaviors.setup[PlanetSidePacket](context => {
|
Behaviors.setup[PlanetSidePacket](context => {
|
||||||
val uuid = randomUUID().toString
|
val uuid = randomUUID().toString
|
||||||
val actor = context.actorOf(classic.Props(new SessionActor(ref, connectionId, Session.getNewId())), s"session-$uuid")
|
val actor =
|
||||||
|
context.actorOf(classic.Props(new SessionActor(ref, connectionId, Session.getNewId())), s"session-$uuid")
|
||||||
Behaviors.receiveMessage(message => {
|
Behaviors.receiveMessage(message => {
|
||||||
actor ! message
|
actor ! message
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
@ -119,7 +117,7 @@ object Server {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
val zones = Zones.zones ++ Seq(Zone.Nowhere)
|
||||||
val serviceManager = ServiceManager.boot
|
val serviceManager = ServiceManager.boot
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
|
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService](), "accountIntermediary")
|
||||||
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy")
|
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService](), "galaxy")
|
||||||
|
|
@ -156,6 +154,8 @@ object Server {
|
||||||
// TODO: clean up active sessions and close resources safely
|
// TODO: clean up active sessions and close resources safely
|
||||||
logger.info("Login server now shutting down...")
|
logger.info("Login server now shutting down...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Await.ready(Future.never, Duration.Inf)
|
||||||
}
|
}
|
||||||
|
|
||||||
def flyway(args: CliConfig): Flyway = {
|
def flyway(args: CliConfig): Flyway = {
|
||||||
|
|
@ -228,6 +228,7 @@ object Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed trait AuthoritativeCounter {
|
sealed trait AuthoritativeCounter {
|
||||||
|
|
||||||
/** the id accumulator */
|
/** the id accumulator */
|
||||||
private val masterIdKeyRing: AtomicLong = new AtomicLong(0L)
|
private val masterIdKeyRing: AtomicLong = new AtomicLong(0L)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ akka {
|
||||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||||
loglevel = INFO
|
loglevel = INFO
|
||||||
logging-filter = akka.event.slf4j.Slf4jLoggingFilter
|
logging-filter = akka.event.slf4j.Slf4jLoggingFilter
|
||||||
|
log-dead-letters-during-shutdown = off
|
||||||
}
|
}
|
||||||
|
|
||||||
akka.actor.deployment {
|
akka.actor.deployment {
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,13 @@ database {
|
||||||
|
|
||||||
# Enable non-standard game properties
|
# Enable non-standard game properties
|
||||||
game {
|
game {
|
||||||
# Allow instant action to AMS
|
instant-action = {
|
||||||
instant-action-ams = no
|
# Allow instant action to direct a player to a hotspot-local friendly AMS
|
||||||
|
spawn-on-ams = no
|
||||||
|
|
||||||
|
# If no ally hotspots can be found during instant action, find a friendly spawn closest to an enemy hot spot
|
||||||
|
third-party = no
|
||||||
|
}
|
||||||
|
|
||||||
# Battle experience rate
|
# Battle experience rate
|
||||||
bep-rate = 1.0
|
bep-rate = 1.0
|
||||||
|
|
@ -212,6 +217,8 @@ game {
|
||||||
# while additional entries with insufficient time spacing may result in no change in behavior.
|
# while additional entries with insufficient time spacing may result in no change in behavior.
|
||||||
delays = [350, 600, 800]
|
delays = [350, 600, 800]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doors-can-be-opened-by-med-app-from-this-distance = 5.05
|
||||||
}
|
}
|
||||||
|
|
||||||
anti-cheat {
|
anti-cheat {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ class LoginActor(
|
||||||
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
||||||
extends Actor
|
extends Actor
|
||||||
with MDCContextAware {
|
with MDCContextAware {
|
||||||
private[this] val log = org.log4s.getLogger
|
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
||||||
|
|
@ -100,9 +99,9 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
|
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
|
||||||
|
|
||||||
if (token.isDefined)
|
if (token.isDefined)
|
||||||
log.trace(s"New login UN:$username Token:${token.get}. $clientVersion")
|
log.debug(s"New login UN:$username Token:${token.get}. $clientVersion")
|
||||||
else {
|
else {
|
||||||
log.trace(s"New login UN:$username. $clientVersion")
|
log.debug(s"New login UN:$username. $clientVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
accountLogin(username, password.getOrElse(""))
|
accountLogin(username, password.getOrElse(""))
|
||||||
|
|
@ -114,7 +113,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
middlewareActor ! MiddlewareActor.Close()
|
middlewareActor ! MiddlewareActor.Close()
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
log.warn(s"Unhandled GamePacket $pkt")
|
log.warning(s"Unhandled GamePacket $pkt")
|
||||||
}
|
}
|
||||||
|
|
||||||
def accountLogin(username: String, password: String): Unit = {
|
def accountLogin(username: String, password: String): Unit = {
|
||||||
|
|
@ -196,7 +195,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
}
|
}
|
||||||
|
|
||||||
def loginPwdFailureResponse(username: String, newToken: String) = {
|
def loginPwdFailureResponse(username: String, newToken: String) = {
|
||||||
log.warn(s"Failed login to account $username")
|
log.warning(s"Failed login to account $username")
|
||||||
middlewareActor ! MiddlewareActor.Send(
|
middlewareActor ! MiddlewareActor.Send(
|
||||||
LoginRespMessage(
|
LoginRespMessage(
|
||||||
newToken,
|
newToken,
|
||||||
|
|
@ -211,7 +210,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
}
|
}
|
||||||
|
|
||||||
def loginFailureResponse(username: String, newToken: String) = {
|
def loginFailureResponse(username: String, newToken: String) = {
|
||||||
log.warn("DB problem")
|
log.warning("DB problem")
|
||||||
middlewareActor ! MiddlewareActor.Send(
|
middlewareActor ! MiddlewareActor.Send(
|
||||||
LoginRespMessage(
|
LoginRespMessage(
|
||||||
newToken,
|
newToken,
|
||||||
|
|
@ -226,7 +225,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
}
|
}
|
||||||
|
|
||||||
def loginAccountFailureResponse(username: String, newToken: String) = {
|
def loginAccountFailureResponse(username: String, newToken: String) = {
|
||||||
log.warn(s"Account $username inactive")
|
log.warning(s"Account $username inactive")
|
||||||
middlewareActor ! MiddlewareActor.Send(
|
middlewareActor ! MiddlewareActor.Send(
|
||||||
LoginRespMessage(
|
LoginRespMessage(
|
||||||
newToken,
|
newToken,
|
||||||
|
|
|
||||||
|
|
@ -193,10 +193,8 @@ class MiddlewareActor(
|
||||||
/** Queue of outgoing packets ready for sending */
|
/** Queue of outgoing packets ready for sending */
|
||||||
val outQueueBundled: mutable.Queue[PlanetSidePacket] = mutable.Queue()
|
val outQueueBundled: mutable.Queue[PlanetSidePacket] = mutable.Queue()
|
||||||
|
|
||||||
/** Latest outbound sequence number;
|
/** Latest outbound sequence number */
|
||||||
* the current sequence is one less than this number
|
var outSequence = -1
|
||||||
*/
|
|
||||||
var outSequence = 0
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment the outbound sequence number.
|
* Increment the outbound sequence number.
|
||||||
|
|
@ -205,13 +203,18 @@ class MiddlewareActor(
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
def nextSequence: Int = {
|
def nextSequence: Int = {
|
||||||
val r = outSequence
|
if (outSequence >= 0xffff) {
|
||||||
if (outSequence == 0xffff) {
|
// TODO resetting the sequence to 0 causes a client crash
|
||||||
outSequence = 0
|
// but that does not happen when we always send the same number
|
||||||
} else {
|
// the solution is most likely to send the proper ResetSequence payload
|
||||||
outSequence += 1
|
// send(ResetSequence(), None, crypto)
|
||||||
|
|
||||||
|
// outSequence = -1
|
||||||
|
// return nextSequence
|
||||||
|
return outSequence
|
||||||
}
|
}
|
||||||
r
|
outSequence += 1
|
||||||
|
outSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Latest outbound subslot number;
|
/** Latest outbound subslot number;
|
||||||
|
|
@ -302,14 +305,14 @@ class MiddlewareActor(
|
||||||
Unknown30 is used to reuse an existing crypto session when switching from login to world
|
Unknown30 is used to reuse an existing crypto session when switching from login to world
|
||||||
When not handling it, it appears that the client will fall back to using ClientStart
|
When not handling it, it appears that the client will fall back to using ClientStart
|
||||||
Do we need to implement this?
|
Do we need to implement this?
|
||||||
*/
|
*/
|
||||||
connectionClose()
|
connectionClose()
|
||||||
|
|
||||||
case (ConnectionClose(), _) =>
|
case (ConnectionClose(), _) =>
|
||||||
/*
|
/*
|
||||||
indicates the user has willingly quit the game world
|
indicates the user has willingly quit the game world
|
||||||
we do not need to implement this
|
we do not need to implement this
|
||||||
*/
|
*/
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
// TODO ResetSequence
|
// TODO ResetSequence
|
||||||
|
|
@ -454,6 +457,11 @@ class MiddlewareActor(
|
||||||
case Successful((packet, Some(sequence))) =>
|
case Successful((packet, Some(sequence))) =>
|
||||||
activeSequenceFunc(packet, sequence)
|
activeSequenceFunc(packet, sequence)
|
||||||
case Successful((packet, None)) =>
|
case Successful((packet, None)) =>
|
||||||
|
packet match {
|
||||||
|
case _: PlanetSideResetSequencePacket =>
|
||||||
|
log.info(s"ResetSequence: ${msg.toHex}, inSeq: ${inSequence}, outSeq: ${outSequence}")
|
||||||
|
case _ => ()
|
||||||
|
}
|
||||||
in(packet)
|
in(packet)
|
||||||
case Failure(e) =>
|
case Failure(e) =>
|
||||||
log.error(s"Could not decode $connectionId's packet: $e")
|
log.error(s"Could not decode $connectionId's packet: $e")
|
||||||
|
|
@ -569,9 +577,14 @@ class MiddlewareActor(
|
||||||
log.error(s"Unexpected crypto packet '$packet'")
|
log.error(s"Unexpected crypto packet '$packet'")
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case _: PlanetSideResetSequencePacket =>
|
case packet: PlanetSideResetSequencePacket =>
|
||||||
log.debug("Received sequence reset request from client; complying")
|
// TODO This is wrong
|
||||||
outSequence = 0
|
// I suspect ResetSequence is a notification that the remote sequence has been reset
|
||||||
|
// rather than a request to reset our outgoing sequence number
|
||||||
|
// Resetting it this way causes a client crash, see nextSequence
|
||||||
|
|
||||||
|
// log.debug(s"Received sequence reset request from client: $packet.}")
|
||||||
|
// outSequence = 0
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ package net.psforever.actors.session
|
||||||
|
|
||||||
import akka.actor.typed.receptionist.Receptionist
|
import akka.actor.typed.receptionist.Receptionist
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import akka.actor.{Actor, MDCContextAware, SupervisorStrategy, typed}
|
import akka.actor.{Actor, MDCContextAware, typed}
|
||||||
import org.joda.time.LocalDateTime
|
import org.joda.time.LocalDateTime
|
||||||
import org.log4s.MDC
|
import org.log4s.MDC
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
@ -102,7 +102,10 @@ object SessionActor {
|
||||||
tickTime: Long = 250L
|
tickTime: Long = 250L
|
||||||
)
|
)
|
||||||
|
|
||||||
private[session] final case class AvatarAwardMessageBundle(bundle: Iterable[Iterable[PlanetSideGamePacket]], delay: Long)
|
private[session] final case class AvatarAwardMessageBundle(
|
||||||
|
bundle: Iterable[Iterable[PlanetSideGamePacket]],
|
||||||
|
delay: Long
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
||||||
|
|
@ -110,11 +113,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
with MDCContextAware {
|
with MDCContextAware {
|
||||||
MDC("connectionId") = connectionId
|
MDC("connectionId") = connectionId
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
|
||||||
private[this] val buffer: mutable.ListBuffer[Any] = new mutable.ListBuffer[Any]()
|
private[this] val buffer: mutable.ListBuffer[Any] = new mutable.ListBuffer[Any]()
|
||||||
private[this] val sessionFuncs = new SessionData(middlewareActor, context)
|
private[this] val sessionFuncs = new SessionData(middlewareActor, context)
|
||||||
|
|
||||||
override val supervisorStrategy: SupervisorStrategy = sessionFuncs.sessionSupervisorStrategy
|
|
||||||
|
|
||||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||||
ServiceManager.serviceManager ! Lookup("accountPersistence")
|
ServiceManager.serviceManager ! Lookup("accountPersistence")
|
||||||
|
|
@ -195,7 +195,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
sessionFuncs.zoning.spawn.handlePlayerLoaded(tplayer)
|
sessionFuncs.zoning.spawn.handlePlayerLoaded(tplayer)
|
||||||
|
|
||||||
case Zone.Population.PlayerHasLeft(zone, None) =>
|
case Zone.Population.PlayerHasLeft(zone, None) =>
|
||||||
log.trace(s"PlayerHasLeft: ${sessionFuncs.player.Name} does not have a body on ${zone.id}")
|
log.debug(s"PlayerHasLeft: ${sessionFuncs.player.Name} does not have a body on ${zone.id}")
|
||||||
|
|
||||||
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
|
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
|
||||||
if (tplayer.isAlive) {
|
if (tplayer.isAlive) {
|
||||||
|
|
@ -203,16 +203,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
}
|
}
|
||||||
|
|
||||||
case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
|
case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
|
||||||
log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
|
log.warning(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
|
||||||
|
|
||||||
case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
|
case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
|
||||||
log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
|
log.warning(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
|
||||||
|
|
||||||
case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
|
case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
|
||||||
log.warn(s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason")
|
log.warning(
|
||||||
|
s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
|
||||||
|
)
|
||||||
|
|
||||||
case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
|
case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
|
||||||
log.warn(s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason")
|
log.warning(
|
||||||
|
s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
|
||||||
|
)
|
||||||
|
|
||||||
case ICS.ZoneResponse(Some(zone)) =>
|
case ICS.ZoneResponse(Some(zone)) =>
|
||||||
sessionFuncs.zoning.handleZoneResponse(zone)
|
sessionFuncs.zoning.handleZoneResponse(zone)
|
||||||
|
|
@ -349,7 +353,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
log.debug(s"CanNotPutItemInSlot: $msg")
|
log.debug(s"CanNotPutItemInSlot: $msg")
|
||||||
|
|
||||||
case default =>
|
case default =>
|
||||||
log.warn(s"Invalid packet class received: $default from ${sender()}")
|
log.warning(s"Invalid packet class received: $default from ${sender()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private def handleGamePkt: PlanetSideGamePacket => Unit = {
|
private def handleGamePkt: PlanetSideGamePacket => Unit = {
|
||||||
|
|
@ -363,7 +367,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
sessionFuncs.vehicles.handleDismountVehicleCargo(packet)
|
sessionFuncs.vehicles.handleDismountVehicleCargo(packet)
|
||||||
|
|
||||||
case packet: CharacterCreateRequestMessage =>
|
case packet: CharacterCreateRequestMessage =>
|
||||||
sessionFuncs.handleCharacterCreateRequest(packet)
|
sessionFuncs.handleCharacterCreateRequest(packet)
|
||||||
|
|
||||||
case packet: CharacterRequestMessage =>
|
case packet: CharacterRequestMessage =>
|
||||||
sessionFuncs.handleCharacterRequest(packet)
|
sessionFuncs.handleCharacterRequest(packet)
|
||||||
|
|
@ -588,6 +592,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
sessionFuncs.handleHitHint(packet)
|
sessionFuncs.handleHitHint(packet)
|
||||||
|
|
||||||
case pkt =>
|
case pkt =>
|
||||||
log.warn(s"Unhandled GamePacket $pkt")
|
log.warning(s"Unhandled GamePacket $pkt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
package net.psforever.actors.session.support
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import akka.actor.{ActorContext, ActorRef, Cancellable, OneForOneStrategy, SupervisorStrategy, typed}
|
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
@ -19,11 +19,10 @@ import net.psforever.objects.avatar._
|
||||||
import net.psforever.objects.ballistics._
|
import net.psforever.objects.ballistics._
|
||||||
import net.psforever.objects.ce._
|
import net.psforever.objects.ce._
|
||||||
import net.psforever.objects.definition._
|
import net.psforever.objects.definition._
|
||||||
import net.psforever.objects.entity.{NoGUIDException,WorldEntity}
|
import net.psforever.objects.entity.WorldEntity
|
||||||
import net.psforever.objects.equipment._
|
import net.psforever.objects.equipment._
|
||||||
import net.psforever.objects.guid._
|
import net.psforever.objects.guid._
|
||||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||||
import net.psforever.objects.loadouts.InfantryLoadout
|
|
||||||
import net.psforever.objects.locker.LockerContainer
|
import net.psforever.objects.locker.LockerContainer
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
import net.psforever.objects.serverobject.deploy.Deployment
|
import net.psforever.objects.serverobject.deploy.Deployment
|
||||||
|
|
@ -65,6 +64,7 @@ import net.psforever.types._
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.Config
|
||||||
|
|
||||||
object SessionData {
|
object SessionData {
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { }
|
private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,7 +540,7 @@ class SessionData(
|
||||||
}
|
}
|
||||||
validObject(pkt.object_guid, decorator = "UseItem") match {
|
validObject(pkt.object_guid, decorator = "UseItem") match {
|
||||||
case Some(door: Door) =>
|
case Some(door: Door) =>
|
||||||
handleUseDoor(door)
|
handleUseDoor(door, equipment)
|
||||||
case Some(resourceSilo: ResourceSilo) =>
|
case Some(resourceSilo: ResourceSilo) =>
|
||||||
handleUseResourceSilo(resourceSilo, equipment)
|
handleUseResourceSilo(resourceSilo, equipment)
|
||||||
case Some(panel: IFFLock) =>
|
case Some(panel: IFFLock) =>
|
||||||
|
|
@ -1122,212 +1122,6 @@ class SessionData(
|
||||||
|
|
||||||
/* supporting functions */
|
/* supporting functions */
|
||||||
|
|
||||||
val sessionSupervisorStrategy: SupervisorStrategy = {
|
|
||||||
import net.psforever.objects.inventory.InventoryDisarrayException
|
|
||||||
OneForOneStrategy(maxNrOfRetries = -1, withinTimeRange = 1 minute) {
|
|
||||||
case nge: NoGUIDException =>
|
|
||||||
nge.getEntity match {
|
|
||||||
case p: Player =>
|
|
||||||
continent.GUID(p.VehicleSeated) match {
|
|
||||||
case Some(v: Vehicle) =>
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsVehicle(v, nge)
|
|
||||||
case _ =>
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(p, nge)
|
|
||||||
}
|
|
||||||
|
|
||||||
case v: Vehicle =>
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsVehicle(v, nge)
|
|
||||||
|
|
||||||
case e: Equipment =>
|
|
||||||
(
|
|
||||||
player.Holsters().zipWithIndex.flatMap { case (o, i) =>
|
|
||||||
o.Equipment.flatMap { item => Some((player, InventoryItem(item, i))) }
|
|
||||||
}.toList ++
|
|
||||||
player.Inventory.Items.map { o => (player, o) } ++
|
|
||||||
player.FreeHand.Equipment.flatMap { item => Some((player, InventoryItem(item, Player.FreeHandSlot))) }.toList ++
|
|
||||||
(validObject(player.VehicleSeated) match {
|
|
||||||
case Some(v: Vehicle) => v.Trunk.Items.map { o => (v, o) }
|
|
||||||
case _ => Nil
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.find { case (_, InventoryItem(o, _)) => o eq e } match {
|
|
||||||
case Some((p: Player, InventoryItem(obj, index))) if !obj.HasGUID =>
|
|
||||||
p.Slot(index).Equipment = None
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(p, nge)
|
|
||||||
case Some((p: Player, _)) =>
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(p, nge)
|
|
||||||
|
|
||||||
case Some((v: Vehicle, InventoryItem(obj, index))) if !obj.HasGUID && v.PassengerInSeat(player).contains(0) =>
|
|
||||||
v.Slot(index).Equipment = None
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(player, nge)
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case Some((v: Vehicle, InventoryItem(obj, index))) if !obj.HasGUID =>
|
|
||||||
v.Slot(index).Equipment = None
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case Some((v: Vehicle, _)) if v.PassengerInSeat(player).contains(0) =>
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(player, nge)
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case Some((_: Vehicle, _)) =>
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
|
|
||||||
case _ =>
|
|
||||||
//did not discover or resolve the situation
|
|
||||||
writeLogExceptionAndStop(nge)
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
}
|
|
||||||
|
|
||||||
case ide: InventoryDisarrayException =>
|
|
||||||
attemptRecoveryFromInventoryDisarrayException(ide.inventory)
|
|
||||||
//re-evaluate results
|
|
||||||
if (ide.inventory.ElementsOnGridMatchList() > 0) {
|
|
||||||
writeLogExceptionAndStop(ide)
|
|
||||||
} else {
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
}
|
|
||||||
|
|
||||||
case e =>
|
|
||||||
writeLogExceptionAndStop(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def attemptRecoveryFromNoGuidExceptionAsVehicle(v: Vehicle, e: Throwable): SupervisorStrategy.Directive = {
|
|
||||||
val entry = v.Seats.find { case (_, s) => s.occupants.contains(player) }
|
|
||||||
entry match {
|
|
||||||
case Some(_) =>
|
|
||||||
player.VehicleSeated = None
|
|
||||||
v.Seats(0).unmount(player)
|
|
||||||
player.Position = v.Position
|
|
||||||
player.Orientation = v.Orientation
|
|
||||||
zoning.interstellarFerry = None
|
|
||||||
zoning.interstellarFerryTopLevelGUID = None
|
|
||||||
attemptRecoveryFromNoGuidExceptionAsPlayer(player, e)
|
|
||||||
case None =>
|
|
||||||
writeLogException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def attemptRecoveryFromNoGuidExceptionAsPlayer(p: Player, e: Throwable): SupervisorStrategy.Directive = {
|
|
||||||
if (p eq player) {
|
|
||||||
val hasGUID = p.HasGUID
|
|
||||||
zoning.zoneLoaded match {
|
|
||||||
case Some(true) if hasGUID =>
|
|
||||||
zoning.spawn.AvatarCreate() //this will probably work?
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case Some(false) =>
|
|
||||||
zoning.RequestSanctuaryZoneSpawn(p, continent.Number)
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case None if player.Zone eq Zone.Nowhere =>
|
|
||||||
zoning.RequestSanctuaryZoneSpawn(p, continent.Number)
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case None =>
|
|
||||||
zoning.zoneReload = true
|
|
||||||
zoning.LoadZoneAsPlayer(player, player.Zone.id)
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
case _ =>
|
|
||||||
writeLogExceptionAndStop(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SupervisorStrategy.resume
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def attemptRecoveryFromInventoryDisarrayException(inv: GridInventory): Unit = {
|
|
||||||
inv.ElementsInListCollideInGrid() match {
|
|
||||||
case Nil => ()
|
|
||||||
case list if list.isEmpty => ()
|
|
||||||
case overlaps =>
|
|
||||||
val previousItems = inv.Clear()
|
|
||||||
val allOverlaps = overlaps.flatten.sortBy { entry =>
|
|
||||||
val tile = entry.obj.Definition.Tile
|
|
||||||
tile.Width * tile.Height
|
|
||||||
}.toSet
|
|
||||||
val notCollidingRemainder = previousItems.filterNot(allOverlaps.contains)
|
|
||||||
notCollidingRemainder.foreach { entry =>
|
|
||||||
inv.InsertQuickly(entry.start, entry.obj)
|
|
||||||
}
|
|
||||||
var didNotFit: List[Equipment] = Nil
|
|
||||||
allOverlaps.foreach { entry =>
|
|
||||||
inv.Fit(entry.obj.Definition.Tile) match {
|
|
||||||
case Some(newStart) =>
|
|
||||||
inv.InsertQuickly(newStart, entry.obj)
|
|
||||||
case None =>
|
|
||||||
didNotFit = didNotFit :+ entry.obj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//completely clear the inventory
|
|
||||||
val pguid = player.GUID
|
|
||||||
val equipmentInHand = player.Slot(player.DrawnSlot).Equipment
|
|
||||||
//redraw suit
|
|
||||||
sendResponse(ArmorChangedMessage(
|
|
||||||
pguid,
|
|
||||||
player.ExoSuit,
|
|
||||||
InfantryLoadout.DetermineSubtypeA(player.ExoSuit, equipmentInHand)
|
|
||||||
))
|
|
||||||
//redraw item in free hand (if)
|
|
||||||
player.FreeHand.Equipment.foreach { item =>
|
|
||||||
sendResponse(ObjectCreateDetailedMessage(
|
|
||||||
item.Definition.ObjectId,
|
|
||||||
item.GUID,
|
|
||||||
ObjectCreateMessageParent(pguid, Player.FreeHandSlot),
|
|
||||||
item.Definition.Packet.DetailedConstructorData(item).get
|
|
||||||
))
|
|
||||||
}
|
|
||||||
//redraw items in holsters
|
|
||||||
player.Holsters().zipWithIndex.foreach { case (slot, _) =>
|
|
||||||
slot.Equipment.foreach { item =>
|
|
||||||
sendResponse(ObjectCreateDetailedMessage(
|
|
||||||
item.Definition.ObjectId,
|
|
||||||
item.GUID,
|
|
||||||
item.Definition.Packet.DetailedConstructorData(item).get
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//redraw raised hand (if)
|
|
||||||
equipmentInHand.foreach { _ =>
|
|
||||||
sendResponse(ObjectHeldMessage(pguid, player.DrawnSlot, unk1 = true))
|
|
||||||
}
|
|
||||||
//redraw inventory items
|
|
||||||
val recoveredItems = inv.Items
|
|
||||||
recoveredItems.foreach { entry =>
|
|
||||||
val item = entry.obj
|
|
||||||
sendResponse(ObjectCreateDetailedMessage(
|
|
||||||
item.Definition.ObjectId,
|
|
||||||
item.GUID,
|
|
||||||
ObjectCreateMessageParent(pguid, entry.start),
|
|
||||||
item.Definition.Packet.DetailedConstructorData(item).get
|
|
||||||
))
|
|
||||||
}
|
|
||||||
//drop items that did not fit
|
|
||||||
val placementData = PlacementData(player.Position, Vector3.z(player.Orientation.z))
|
|
||||||
didNotFit.foreach { item =>
|
|
||||||
sendResponse(ObjectCreateMessage(
|
|
||||||
item.Definition.ObjectId,
|
|
||||||
item.GUID,
|
|
||||||
DroppedItemData(
|
|
||||||
placementData,
|
|
||||||
item.Definition.Packet.ConstructorData(item).get
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeLogException(e: Throwable): SupervisorStrategy.Directive = {
|
|
||||||
import java.io.{PrintWriter, StringWriter}
|
|
||||||
val sw = new StringWriter
|
|
||||||
e.printStackTrace(new PrintWriter(sw))
|
|
||||||
log.error(sw.toString)
|
|
||||||
SupervisorStrategy.Resume
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeLogExceptionAndStop(e: Throwable): SupervisorStrategy.Directive = {
|
|
||||||
writeLogException(e)
|
|
||||||
immediateDisconnect()
|
|
||||||
SupervisorStrategy.stop
|
|
||||||
}
|
|
||||||
|
|
||||||
def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = {
|
def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = {
|
||||||
if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) {
|
if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) {
|
||||||
galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionData=this, avatarActor, galaxyActor, context))
|
galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionData=this, avatarActor, galaxyActor, context))
|
||||||
|
|
@ -1451,8 +1245,17 @@ class SessionData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def handleUseDoor(door: Door): Unit = {
|
private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
|
||||||
door.Actor ! CommonMessages.Use(player)
|
equipment match {
|
||||||
|
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
|
||||||
|
val distance: Float = math.max(
|
||||||
|
Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance,
|
||||||
|
door.Definition.initialOpeningDistance
|
||||||
|
)
|
||||||
|
door.Actor ! CommonMessages.Use(player, Some(distance))
|
||||||
|
case _ =>
|
||||||
|
door.Actor ! CommonMessages.Use(player)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
|
private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
|
||||||
|
|
|
||||||
|
|
@ -45,52 +45,7 @@ class SessionGalaxyHandlers(
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
|
case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
|
||||||
val playerName = player.Name
|
sessionData.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
|
||||||
log.debug(s"TransferPassenger: $playerName received the summons to transfer to ${vehicle.Zone.id} ...")
|
|
||||||
(manifest.passengers.find { _.name.equals(playerName) } match {
|
|
||||||
case Some(entry) if vehicle.Seats(entry.mount).occupant.isEmpty =>
|
|
||||||
player.VehicleSeated = None
|
|
||||||
vehicle.Seats(entry.mount).mount(player)
|
|
||||||
player.VehicleSeated = vehicle.GUID
|
|
||||||
Some(vehicle)
|
|
||||||
case Some(entry) if vehicle.Seats(entry.mount).occupant.contains(player) =>
|
|
||||||
Some(vehicle)
|
|
||||||
case Some(entry) =>
|
|
||||||
log.warn(
|
|
||||||
s"TransferPassenger: $playerName tried to mount seat ${entry.mount} during summoning, but it was already occupied, and ${player.Sex.pronounSubject} was rebuked"
|
|
||||||
)
|
|
||||||
None
|
|
||||||
case None =>
|
|
||||||
//log.warn(s"TransferPassenger: $playerName is missing from the manifest of a summoning ${vehicle.Definition.Name} from ${vehicle.Zone.id}")
|
|
||||||
None
|
|
||||||
}).orElse {
|
|
||||||
manifest.cargo.find { _.name.equals(playerName) } match {
|
|
||||||
case Some(entry) =>
|
|
||||||
vehicle.CargoHolds(entry.mount).occupant match {
|
|
||||||
case out @ Some(cargo) if cargo.Seats(0).occupants.exists(_.Name.equals(playerName)) =>
|
|
||||||
out
|
|
||||||
case _ =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} match {
|
|
||||||
case Some(v: Vehicle) =>
|
|
||||||
galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above)
|
|
||||||
sessionData.zoning.spawn.deadState = DeadState.Release
|
|
||||||
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
|
|
||||||
sessionData.zoning.interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system
|
|
||||||
sessionData.zoning.spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None)
|
|
||||||
case _ =>
|
|
||||||
sessionData.zoning.interstellarFerry match {
|
|
||||||
case None =>
|
|
||||||
galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
|
|
||||||
sessionData.zoning.interstellarFerryTopLevelGUID = None
|
|
||||||
case Some(_) => ;
|
|
||||||
//wait patiently
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
|
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
|
||||||
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
|
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
|
||||||
|
|
@ -103,13 +58,13 @@ class SessionGalaxyHandlers(
|
||||||
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
||||||
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
||||||
|
|
||||||
case GalaxyResponse.LogStatusChange(name) =>
|
case GalaxyResponse.LogStatusChange(name) if (avatar.people.friend.exists { _.name.equals(name) }) =>
|
||||||
if (avatar.people.friend.exists { _.name.equals(name) }) {
|
avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
|
||||||
avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
case GalaxyResponse.SendResponse(msg) =>
|
case GalaxyResponse.SendResponse(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,8 @@ class VehicleOperations(
|
||||||
obj.PassengerInSeat(player) match {
|
obj.PassengerInSeat(player) match {
|
||||||
case Some(seat_num) =>
|
case Some(seat_num) =>
|
||||||
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
|
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
|
||||||
if (sessionData.zoning.interstellarFerry.isDefined) {
|
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
|
||||||
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
|
sessionData.zoning.interstellarFerry = None
|
||||||
//see above in VehicleResponse.TransferPassenger case
|
|
||||||
sessionData.zoning.interstellarFerry = None
|
|
||||||
}
|
|
||||||
// Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
|
// Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
|
||||||
//todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
|
//todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
|
||||||
//todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
|
//todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
|
||||||
|
|
@ -626,6 +623,7 @@ class VehicleOperations(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
|
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
|
||||||
serverVehicleControlVelocity = None
|
serverVehicleControlVelocity = None
|
||||||
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false, lock_wheel=false, reverse=false, unk4=false, 0, 0, 0, None))
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false, lock_wheel=false, reverse=false, unk4=false, 0, 0, 0, None))
|
||||||
|
|
|
||||||
|
|
@ -762,19 +762,19 @@ class ZoningOperations(
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} match {
|
} match {
|
||||||
case Some(v: Vehicle) =>
|
case Some(cargo: Vehicle) =>
|
||||||
galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above)
|
galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above)
|
||||||
spawn.deadState = DeadState.Release
|
spawn.deadState = DeadState.Release
|
||||||
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
|
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
|
||||||
interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system
|
interstellarFerry = Some(cargo) //on the other continent and registered to that continent's GUID system
|
||||||
spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None)
|
cargo.MountedIn = vehicle.GUID
|
||||||
|
spawn.LoadZonePhysicalSpawnPoint(cargo.Continent, cargo.Position, cargo.Orientation, respawnTime = 1 seconds, None)
|
||||||
case _ =>
|
case _ =>
|
||||||
interstellarFerry match {
|
interstellarFerry match {
|
||||||
case None =>
|
case None =>
|
||||||
galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
|
galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
|
||||||
interstellarFerryTopLevelGUID = None
|
interstellarFerryTopLevelGUID = None
|
||||||
case Some(_) => ;
|
case Some(_) => () //wait patiently
|
||||||
//wait patiently
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
src/main/scala/net/psforever/objects/Doors.scala
Normal file
81
src/main/scala/net/psforever/objects/Doors.scala
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.objects
|
||||||
|
|
||||||
|
import net.psforever.objects.serverobject.doors.Door
|
||||||
|
import net.psforever.types.Vector3
|
||||||
|
|
||||||
|
object Doors {
|
||||||
|
/**
|
||||||
|
* If a player was considered as holding the door open,
|
||||||
|
* check that if that player is still in the same zone as a door
|
||||||
|
* and is still standing within a certain distance of that door.
|
||||||
|
* If the target player no longer qualifies to hold the door open,
|
||||||
|
* search for a new player to hold the door open.
|
||||||
|
* If the target player did not initially exist (the door was closed),
|
||||||
|
* do not search any further.
|
||||||
|
* @param door door that is installed somewhere
|
||||||
|
* @param maximumDistance permissible square distance between the player and the door
|
||||||
|
* @return optional player;
|
||||||
|
* `None` if the no player can trigger the door
|
||||||
|
*/
|
||||||
|
def testForTargetHoldingDoorOpen(
|
||||||
|
door: Door,
|
||||||
|
maximumDistance: Float
|
||||||
|
): Option[Player] = {
|
||||||
|
door.Open
|
||||||
|
.flatMap {
|
||||||
|
testForSpecificTargetHoldingDoorOpen(_, door, maximumDistance * maximumDistance)
|
||||||
|
.orElse { testForAnyTargetHoldingDoorOpen(door, maximumDistance) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a player is in the same zone as a door
|
||||||
|
* and is standing within a certain distance of that door.
|
||||||
|
* @see `Vector3.MagnitudeSquared`
|
||||||
|
* @param player player who is standing somewhere
|
||||||
|
* @param door door that is installed somewhere
|
||||||
|
* @param maximumDistance permissible square distance between the player and the door
|
||||||
|
* before one can not influence the other
|
||||||
|
* @return optional player (same as parameter);
|
||||||
|
* `None` if the parameter player can not trigger the door
|
||||||
|
*/
|
||||||
|
def testForSpecificTargetHoldingDoorOpen(
|
||||||
|
player: Player,
|
||||||
|
door: Door,
|
||||||
|
maximumDistance: Float
|
||||||
|
): Option[Player] = {
|
||||||
|
if (player.Zone == door.Zone && Vector3.MagnitudeSquared(door.Position - player.Position) <= maximumDistance) {
|
||||||
|
Some(player)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a player, any player, that can hold the door open.
|
||||||
|
* Prop it open with a dead body if no one is available.
|
||||||
|
* @param door door that is installed somewhere
|
||||||
|
* @param maximumDistance permissible square distance between a player and the door
|
||||||
|
* before one can not influence the other
|
||||||
|
* @return optional player;
|
||||||
|
* `None` if no player can trigger the door
|
||||||
|
*/
|
||||||
|
private def testForAnyTargetHoldingDoorOpen(
|
||||||
|
door: Door,
|
||||||
|
maximumDistance: Float
|
||||||
|
): Option[Player] = {
|
||||||
|
//search for nearby players and nearby former players who would block open the door
|
||||||
|
val zone = door.Zone
|
||||||
|
val maximumDistanceSq = maximumDistance * maximumDistance
|
||||||
|
val bmap = zone.blockMap.sector(door.Position, maximumDistance)
|
||||||
|
(bmap.livePlayerList ++ bmap.corpseList)
|
||||||
|
.find { testForSpecificTargetHoldingDoorOpen(_, door, maximumDistanceSq).nonEmpty } match {
|
||||||
|
case out @ Some(newOpener) =>
|
||||||
|
door.Open = newOpener //another player is near the door, keep it open
|
||||||
|
out
|
||||||
|
case _ =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -256,5 +256,3 @@ object Tool {
|
||||||
def Definition: FireModeDefinition = fdef
|
def Definition: FireModeDefinition = fdef
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,13 +113,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
||||||
private var utilities: Map[Int, Utility] = Map.empty
|
private var utilities: Map[Int, Utility] = Map.empty
|
||||||
private var subsystems: List[VehicleSubsystem] = Nil
|
private var subsystems: List[VehicleSubsystem] = Nil
|
||||||
private val trunk: GridInventory = GridInventory()
|
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 vehicleGatingManifest: Option[VehicleManifest] = None
|
||||||
private var previousVehicleGatingManifest: Option[VehicleManifest] = None
|
private var previousVehicleGatingManifest: Option[VehicleManifest] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.objects.serverobject.doors
|
package net.psforever.objects.serverobject.doors
|
||||||
|
|
||||||
import net.psforever.objects.Player
|
import akka.actor.ActorRef
|
||||||
|
import net.psforever.objects.{Default, Doors, Player}
|
||||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||||
import net.psforever.objects.serverobject.locks.IFFLock
|
import net.psforever.objects.serverobject.locks.IFFLock
|
||||||
|
|
@ -17,8 +18,9 @@ class DoorControl(door: Door)
|
||||||
extends PoweredAmenityControl
|
extends PoweredAmenityControl
|
||||||
with FactionAffinityBehavior.Check {
|
with FactionAffinityBehavior.Check {
|
||||||
def FactionObject: FactionAffinity = door
|
def FactionObject: FactionAffinity = door
|
||||||
var isLocked: Boolean = false
|
|
||||||
var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen
|
private var isLocked: Boolean = false
|
||||||
|
private var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen
|
||||||
|
|
||||||
def commonBehavior: Receive = checkBehavior
|
def commonBehavior: Receive = checkBehavior
|
||||||
.orElse {
|
.orElse {
|
||||||
|
|
@ -40,15 +42,16 @@ class DoorControl(door: Door)
|
||||||
def poweredStateLogic: Receive =
|
def poweredStateLogic: Receive =
|
||||||
commonBehavior
|
commonBehavior
|
||||||
.orElse {
|
.orElse {
|
||||||
|
case CommonMessages.Use(player, Some(customDistance: Float)) =>
|
||||||
|
testToOpenDoor(player, door, customDistance, sender())
|
||||||
|
|
||||||
case CommonMessages.Use(player, _) =>
|
case CommonMessages.Use(player, _) =>
|
||||||
if (lockingMechanism(player, door) && !isLocked) {
|
testToOpenDoor(player, door, door.Definition.initialOpeningDistance, sender())
|
||||||
openDoor(player)
|
|
||||||
}
|
|
||||||
|
|
||||||
case IFFLock.DoorOpenResponse(target: Player) if !isLocked =>
|
case IFFLock.DoorOpenResponse(target: Player) if !isLocked =>
|
||||||
openDoor(target)
|
DoorControl.openDoor(target, door)
|
||||||
|
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
def unpoweredStateLogic: Receive = {
|
def unpoweredStateLogic: Receive = {
|
||||||
|
|
@ -56,30 +59,33 @@ class DoorControl(door: Door)
|
||||||
.orElse {
|
.orElse {
|
||||||
case CommonMessages.Use(player, _) if !isLocked =>
|
case CommonMessages.Use(player, _) if !isLocked =>
|
||||||
//without power, the door opens freely
|
//without power, the door opens freely
|
||||||
openDoor(player)
|
DoorControl.openDoor(player, door)
|
||||||
|
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def openDoor(player: Player): Unit = {
|
/**
|
||||||
val zone = door.Zone
|
* If the player is close enough to the door,
|
||||||
val doorGUID = door.GUID
|
* the locking mechanism allows for the door to open,
|
||||||
if (!door.isOpen) {
|
* and the door is not bolted shut (locked),
|
||||||
//global open
|
* then tell the door that it should open.
|
||||||
door.Open = player
|
* @param player player who is standing somewhere
|
||||||
zone.LocalEvents ! LocalServiceMessage(
|
* @param door door that is installed somewhere
|
||||||
zone.id,
|
* @param maximumDistance permissible square distance between the player and the door
|
||||||
LocalAction.DoorOpens(Service.defaultPlayerGUID, zone, door)
|
* @param replyTo the player's session message reference
|
||||||
)
|
*/
|
||||||
}
|
private def testToOpenDoor(
|
||||||
else {
|
player: Player,
|
||||||
//the door should already open, but the requesting player does not see it as open
|
door: Door,
|
||||||
sender() ! LocalServiceResponse(
|
maximumDistance: Float,
|
||||||
player.Name,
|
replyTo: ActorRef
|
||||||
Service.defaultPlayerGUID,
|
): Unit = {
|
||||||
LocalResponse.DoorOpens(doorGUID)
|
if (
|
||||||
)
|
Doors.testForSpecificTargetHoldingDoorOpen(player, door, maximumDistance * maximumDistance).contains(player) &&
|
||||||
|
lockingMechanism(player, door) && !isLocked
|
||||||
|
) {
|
||||||
|
DoorControl.openDoor(player, door, replyTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,5 +95,33 @@ class DoorControl(door: Door)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoorControl {
|
object DoorControl {
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true
|
def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the door is not open, open this door, propped open by the given player.
|
||||||
|
* If the door is considered open, ensure the door is proper visible as open to the player.
|
||||||
|
* @param player the player
|
||||||
|
* @param door the door
|
||||||
|
* @param replyTo the player's session message reference
|
||||||
|
*/
|
||||||
|
private def openDoor(player: Player, door: Door, replyTo: ActorRef = Default.Actor): Unit = {
|
||||||
|
val zone = door.Zone
|
||||||
|
val doorGUID = door.GUID
|
||||||
|
if (!door.isOpen) {
|
||||||
|
//global open
|
||||||
|
door.Open = player
|
||||||
|
zone.LocalEvents ! LocalServiceMessage(
|
||||||
|
zone.id,
|
||||||
|
LocalAction.DoorOpens(Service.defaultPlayerGUID, zone, door)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
//the door should already open, but the requesting player does not see it as open
|
||||||
|
replyTo ! LocalServiceResponse(
|
||||||
|
player.Name,
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
LocalResponse.DoorOpens(doorGUID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,9 @@ import net.psforever.objects.serverobject.structures.AmenityDefinition
|
||||||
class DoorDefinition(objectId: Int)
|
class DoorDefinition(objectId: Int)
|
||||||
extends AmenityDefinition(objectId) {
|
extends AmenityDefinition(objectId) {
|
||||||
Name = "door"
|
Name = "door"
|
||||||
|
/** range wherein the door may first be opened
|
||||||
|
* (note: intentionally inflated as the initial check on the client occurs further than expected) */
|
||||||
|
var initialOpeningDistance: Float = 7.5f
|
||||||
|
/** range within which the door must detect a target player to remain open */
|
||||||
|
var continuousOpenDistance: Float = 5.05f
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ trait CargoBehavior {
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
isDismounting = None
|
isDismounting = None
|
||||||
startCargoDismounting(bailed = false)
|
startCargoDismountingNoCleanup(bailed = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cargoBehavior: Receive = {
|
val cargoBehavior: Receive = {
|
||||||
|
|
@ -64,24 +64,26 @@ trait CargoBehavior {
|
||||||
}
|
}
|
||||||
|
|
||||||
def startCargoDismounting(bailed: Boolean): Unit = {
|
def startCargoDismounting(bailed: Boolean): Unit = {
|
||||||
val obj = CargoObject
|
if (!startCargoDismountingNoCleanup(bailed)) {
|
||||||
obj.Zone.GUID(obj.MountedIn) match {
|
isDismounting = None
|
||||||
case Some(carrier: Vehicle) =>
|
CargoObject.MountedIn = None
|
||||||
carrier.CargoHolds.find { case (_, hold) => hold.occupant.contains(obj) } match {
|
|
||||||
case Some((mountPoint, _))
|
|
||||||
if isDismounting.isEmpty && isMounting.isEmpty =>
|
|
||||||
isDismounting = obj.MountedIn
|
|
||||||
carrier.Actor ! CarrierBehavior.CheckCargoDismount(obj.GUID, mountPoint, 0, bailed)
|
|
||||||
|
|
||||||
case _ =>
|
|
||||||
obj.MountedIn = None
|
|
||||||
isDismounting = None
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
obj.MountedIn = None
|
|
||||||
isDismounting = None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def startCargoDismountingNoCleanup(bailed: Boolean): Boolean = {
|
||||||
|
val obj = CargoObject
|
||||||
|
obj.Zone.GUID(obj.MountedIn)
|
||||||
|
.collect { case carrier: Vehicle =>
|
||||||
|
(carrier, carrier.CargoHolds.find { case (_, hold) => hold.occupant.contains(obj) })
|
||||||
|
}
|
||||||
|
.collect { case (carrier, Some((mountPoint, _)))
|
||||||
|
if isDismounting.isEmpty && isMounting.isEmpty =>
|
||||||
|
isDismounting = obj.MountedIn
|
||||||
|
carrier.Actor ! CarrierBehavior.CheckCargoDismount(obj.GUID, mountPoint, 0, bailed)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.nonEmpty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object CargoBehavior {
|
object CargoBehavior {
|
||||||
|
|
|
||||||
|
|
@ -654,4 +654,3 @@ object CarrierBehavior {
|
||||||
msgs
|
msgs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,3 @@ trait MountableWeapons
|
||||||
|
|
||||||
def Definition: MountableWeaponsDefinition
|
def Definition: MountableWeaponsDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,4 +55,3 @@ class AmsControl(vehicle: Vehicle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ class CargoCarrierControl(vehicle: Vehicle)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the vehicle becomes disabled, the safety and autonomy of the cargo should be prioritized.
|
* If the vehicle becomes disabled, the safety and autonomy of the cargo should be prioritized.
|
||||||
* @param kickPassengers passengers need to be ejected "by force"
|
* @param kickPassengers passengers need to be ejected "by force";
|
||||||
|
* actually controls bail protection and flavors a log message (further down the line)
|
||||||
*/
|
*/
|
||||||
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
|
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
|
||||||
|
super.PrepareForDisabled(kickPassengers)
|
||||||
//abandon all cargo
|
//abandon all cargo
|
||||||
vehicle.CargoHolds.collect {
|
vehicle.CargoHolds.collect {
|
||||||
case (index, hold : Cargo) if hold.isOccupied =>
|
case (index, hold : Cargo) if hold.isOccupied =>
|
||||||
val cargo = hold.occupant.get
|
val cargo = hold.occupant.get
|
||||||
checkCargoDismount(cargo.GUID, index, iteration = 0, bailed = false)
|
checkCargoDismount(cargo.GUID, index, iteration = 0, bailed = kickPassengers)
|
||||||
super.PrepareForDisabled(kickPassengers)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@ class VehicleControl(vehicle: Vehicle)
|
||||||
//escape being someone else's cargo
|
//escape being someone else's cargo
|
||||||
vehicle.MountedIn match {
|
vehicle.MountedIn match {
|
||||||
case Some(_) =>
|
case Some(_) =>
|
||||||
startCargoDismounting(bailed = false)
|
startCargoDismounting(bailed = true)
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
if (!vehicle.isFlying || kickPassengers) {
|
if (!vehicle.isFlying || kickPassengers) {
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,20 @@ object PacketHelpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a Codec for an enumeration type that can correctly represent its value
|
/** Create a Codec for an enumeration type that can correctly represent its value
|
||||||
* @param enum the enumeration type to create a codec for
|
* @param e the enumeration type to create a codec for
|
||||||
* @param storageCodec the Codec used for actually representing the value
|
* @param storageCodec the Codec used for actually representing the value
|
||||||
* @tparam E The inferred type
|
* @tparam E The inferred type
|
||||||
* @return Generated codec
|
* @return Generated codec
|
||||||
*/
|
*/
|
||||||
def createEnumerationCodec[E <: Enumeration](enum: E, storageCodec: Codec[Int]): Codec[E#Value] = {
|
def createEnumerationCodec[E <: Enumeration](e: E, storageCodec: Codec[Int]): Codec[E#Value] = {
|
||||||
type Struct = Int :: HNil
|
type Struct = Int :: HNil
|
||||||
val struct: Codec[Struct] = storageCodec.hlist
|
val struct: Codec[Struct] = storageCodec.hlist
|
||||||
val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get.toDouble)
|
val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get.toDouble)
|
||||||
|
|
||||||
// Assure that the enum will always be able to fit in a N-bit int
|
// Assure that the enum will always be able to fit in a N-bit int
|
||||||
assert(
|
assert(
|
||||||
enum.maxId <= primitiveLimit,
|
e.maxId <= primitiveLimit,
|
||||||
enum.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${enum.maxId})"
|
e.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${e.maxId})"
|
||||||
)
|
)
|
||||||
|
|
||||||
def to(pkt: E#Value): Struct = {
|
def to(pkt: E#Value): Struct = {
|
||||||
|
|
@ -113,13 +113,13 @@ object PacketHelpers {
|
||||||
struct match {
|
struct match {
|
||||||
case enumVal :: HNil =>
|
case enumVal :: HNil =>
|
||||||
// verify that this int can match the enum
|
// verify that this int can match the enum
|
||||||
val first = enum.values.firstKey.id
|
val first = e.values.firstKey.id
|
||||||
val last = enum.maxId - 1
|
val last = e.maxId - 1
|
||||||
|
|
||||||
if (enumVal >= first && enumVal <= last)
|
if (enumVal >= first && enumVal <= last)
|
||||||
Attempt.successful(enum(enumVal))
|
Attempt.successful(e(enumVal))
|
||||||
else
|
else
|
||||||
Attempt.failure(Err(s"Expected ${enum} with ID between [${first}, ${last}], but got '${enumVal}'"))
|
Attempt.failure(Err(s"Expected ${e} with ID between [${first}, ${last}], but got '${enumVal}'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct.narrow[E#Value](from, to)
|
struct.narrow[E#Value](from, to)
|
||||||
|
|
@ -130,12 +130,12 @@ object PacketHelpers {
|
||||||
* NOTE: enumerations in scala can't be represented by more than an Int anyways, so this conversion shouldn't matter.
|
* NOTE: enumerations in scala can't be represented by more than an Int anyways, so this conversion shouldn't matter.
|
||||||
* This is only to overload createEnumerationCodec to work with uint32[L] codecs (which are Long)
|
* This is only to overload createEnumerationCodec to work with uint32[L] codecs (which are Long)
|
||||||
*/
|
*/
|
||||||
def createLongEnumerationCodec[E <: Enumeration](enum: E, storageCodec: Codec[Long]): Codec[E#Value] = {
|
def createLongEnumerationCodec[E <: Enumeration](e: E, storageCodec: Codec[Long]): Codec[E#Value] = {
|
||||||
createEnumerationCodec(enum, storageCodec.xmap[Int](_.toInt, _.toLong))
|
createEnumerationCodec(e, storageCodec.xmap[Int](_.toInt, _.toLong))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a Codec for enumeratum's IntEnum type */
|
/** Create a Codec for enumeratum's IntEnum type */
|
||||||
def createIntEnumCodec[E <: IntEnumEntry](enum: IntEnum[E], storageCodec: Codec[Int]): Codec[E] = {
|
def createIntEnumCodec[E <: IntEnumEntry](e: IntEnum[E], storageCodec: Codec[Int]): Codec[E] = {
|
||||||
type Struct = Int :: HNil
|
type Struct = Int :: HNil
|
||||||
val struct: Codec[Struct] = storageCodec.hlist
|
val struct: Codec[Struct] = storageCodec.hlist
|
||||||
|
|
||||||
|
|
@ -146,36 +146,36 @@ object PacketHelpers {
|
||||||
def from(struct: Struct): Attempt[E] =
|
def from(struct: Struct): Attempt[E] =
|
||||||
struct match {
|
struct match {
|
||||||
case enumVal :: HNil =>
|
case enumVal :: HNil =>
|
||||||
enum.withValueOpt(enumVal) match {
|
e.withValueOpt(enumVal) match {
|
||||||
case Some(v) => Attempt.successful(v)
|
case Some(v) => Attempt.successful(v)
|
||||||
case None =>
|
case None =>
|
||||||
Attempt.failure(Err(s"Enum value '${enumVal}' not found in values '${enum.values.toString()}'"))
|
Attempt.failure(Err(s"Enum value '${enumVal}' not found in values '${e.values.toString()}'"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct.narrow[E](from, to)
|
struct.narrow[E](from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
def createLongIntEnumCodec[E <: IntEnumEntry](enum: IntEnum[E], storageCodec: Codec[Long]): Codec[E] = {
|
def createLongIntEnumCodec[E <: IntEnumEntry](e: IntEnum[E], storageCodec: Codec[Long]): Codec[E] = {
|
||||||
createIntEnumCodec(enum, storageCodec.xmap[Int](_.toInt, _.toLong))
|
createIntEnumCodec(e, storageCodec.xmap[Int](_.toInt, _.toLong))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a Codec for enumeratum's Enum type */
|
/** Create a Codec for enumeratum's Enum type */
|
||||||
def createEnumCodec[E <: EnumEntry](enum: Enum[E], storageCodec: Codec[Int]): Codec[E] = {
|
def createEnumCodec[E <: EnumEntry](e: Enum[E], storageCodec: Codec[Int]): Codec[E] = {
|
||||||
type Struct = Int :: HNil
|
type Struct = Int :: HNil
|
||||||
val struct: Codec[Struct] = storageCodec.hlist
|
val struct: Codec[Struct] = storageCodec.hlist
|
||||||
|
|
||||||
def to(pkt: E): Struct = {
|
def to(pkt: E): Struct = {
|
||||||
enum.indexOf(pkt) :: HNil
|
e.indexOf(pkt) :: HNil
|
||||||
}
|
}
|
||||||
|
|
||||||
def from(struct: Struct): Attempt[E] =
|
def from(struct: Struct): Attempt[E] =
|
||||||
struct match {
|
struct match {
|
||||||
case enumVal :: HNil =>
|
case enumVal :: HNil =>
|
||||||
enum.valuesToIndex.find(_._2 == enumVal) match {
|
e.valuesToIndex.find(_._2 == enumVal) match {
|
||||||
case Some((v, _)) => Attempt.successful(v)
|
case Some((v, _)) => Attempt.successful(v)
|
||||||
case None =>
|
case None =>
|
||||||
Attempt.failure(Err(s"Enum index '${enumVal}' not found in values '${enum.valuesToIndex.toString()}'"))
|
Attempt.failure(Err(s"Enum index '${enumVal}' not found in values '${e.valuesToIndex.toString()}'"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ object PacketCoding {
|
||||||
): Attempt[BitVector] = {
|
): Attempt[BitVector] = {
|
||||||
val seq = packet match {
|
val seq = packet match {
|
||||||
case _: PlanetSideControlPacket if crypto.isEmpty => BitVector.empty
|
case _: PlanetSideControlPacket if crypto.isEmpty => BitVector.empty
|
||||||
|
case _: PlanetSideResetSequencePacket => BitVector.empty
|
||||||
case _ =>
|
case _ =>
|
||||||
sequence match {
|
sequence match {
|
||||||
case Some(_sequence) =>
|
case Some(_sequence) =>
|
||||||
|
|
@ -93,6 +94,17 @@ object PacketCoding {
|
||||||
)
|
)
|
||||||
case f @ Failure(_) => return f
|
case f @ Failure(_) => return f
|
||||||
}
|
}
|
||||||
|
case packet: PlanetSideResetSequencePacket =>
|
||||||
|
encodePacket(packet) match {
|
||||||
|
case Successful(_payload) =>
|
||||||
|
(
|
||||||
|
PlanetSidePacketFlags.codec
|
||||||
|
.encode(PlanetSidePacketFlags(PacketType.ResetSequence, secured = false))
|
||||||
|
.require,
|
||||||
|
_payload
|
||||||
|
)
|
||||||
|
case f @ Failure(_) => return f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Successful(flags ++ seq ++ payload)
|
Successful(flags ++ seq ++ payload)
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,3 @@ final case class Ignore(data: ByteVector) extends PlanetSideCryptoPacket {
|
||||||
object Ignore extends Marshallable[Ignore] {
|
object Ignore extends Marshallable[Ignore] {
|
||||||
implicit val codec: Codec[Ignore] = ("data" | bytes).as[Ignore]
|
implicit val codec: Codec[Ignore] = ("data" | bytes).as[Ignore]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,32 +11,32 @@ sealed abstract class GenericAction(val value: Int) extends IntEnumEntry
|
||||||
object GenericAction extends IntEnum[GenericAction] {
|
object GenericAction extends IntEnum[GenericAction] {
|
||||||
val values: IndexedSeq[GenericAction] = findValues
|
val values: IndexedSeq[GenericAction] = findValues
|
||||||
|
|
||||||
final case object ShowMosquitoRadar extends GenericAction(value = 3)
|
final case object ShowMosquitoRadar extends GenericAction(value = 3)
|
||||||
final case object HideMosquitoRadar extends GenericAction(value = 4)
|
final case object HideMosquitoRadar extends GenericAction(value = 4)
|
||||||
final case object MissileLock extends GenericAction(value = 7)
|
final case object MissileLock extends GenericAction(value = 7)
|
||||||
final case object WaspMissileLock extends GenericAction(value = 8)
|
final case object WaspMissileLock extends GenericAction(value = 8)
|
||||||
final case object TRekLock extends GenericAction(value = 9)
|
final case object TRekLock extends GenericAction(value = 9)
|
||||||
final case object DropSpecialItem extends GenericAction(value = 11)
|
final case object DropSpecialItem extends GenericAction(value = 11)
|
||||||
final case object FacilityCaptureFanfare extends GenericAction(value = 12)
|
final case object FacilityCaptureFanfare extends GenericAction(value = 12)
|
||||||
final case object NewCharacterBasicTrainingPrompt extends GenericAction(value = 14)
|
final case object NewCharacterBasicTrainingPrompt extends GenericAction(value = 14)
|
||||||
final case object MaxAnchorsExtend_RCV extends GenericAction(value = 15)
|
final case object MaxAnchorsExtend_RCV extends GenericAction(value = 15)
|
||||||
final case object MaxAnchorsRelease_RCV extends GenericAction(value = 16)
|
final case object MaxAnchorsRelease_RCV extends GenericAction(value = 16)
|
||||||
final case object MaxSpecialEffect_RCV extends GenericAction(value = 20)
|
final case object MaxSpecialEffect_RCV extends GenericAction(value = 20)
|
||||||
final case object StopMaxSpecialEffect_RCV extends GenericAction(value = 21)
|
final case object StopMaxSpecialEffect_RCV extends GenericAction(value = 21)
|
||||||
final case object CavernFacilityCapture extends GenericAction(value = 22)
|
final case object CavernFacilityCapture extends GenericAction(value = 22)
|
||||||
final case object CavernFacilityKill extends GenericAction(value = 23)
|
final case object CavernFacilityKill extends GenericAction(value = 23)
|
||||||
final case object Imprinted extends GenericAction(value = 24)
|
final case object Imprinted extends GenericAction(value = 24)
|
||||||
final case object NoLongerImprinted extends GenericAction(value = 25)
|
final case object NoLongerImprinted extends GenericAction(value = 25)
|
||||||
final case object PurchaseTimersReset extends GenericAction(value = 27)
|
final case object PurchaseTimersReset extends GenericAction(value = 27)
|
||||||
final case object LeaveWarpQueue_RCV extends GenericAction(value = 28)
|
final case object LeaveWarpQueue_RCV extends GenericAction(value = 28)
|
||||||
final case object AwayFromKeyboard_RCV extends GenericAction(value = 29)
|
final case object AwayFromKeyboard_RCV extends GenericAction(value = 29)
|
||||||
final case object BackInGame_RCV extends GenericAction(value = 30)
|
final case object BackInGame_RCV extends GenericAction(value = 30)
|
||||||
final case object FirstPersonViewWithEffect extends GenericAction(value = 31)
|
final case object FirstPersonViewWithEffect extends GenericAction(value = 31)
|
||||||
final case object FirstPersonViewFailToDeconstruct extends GenericAction(value = 32)
|
final case object FirstPersonViewFailToDeconstruct extends GenericAction(value = 32)
|
||||||
final case object FailToDeconstruct extends GenericAction(value = 33)
|
final case object FailToDeconstruct extends GenericAction(value = 33)
|
||||||
final case object LookingForSquad_RCV extends GenericAction(value = 36)
|
final case object LookingForSquad_RCV extends GenericAction(value = 36)
|
||||||
final case object NotLookingForSquad_RCV extends GenericAction(value = 37)
|
final case object NotLookingForSquad_RCV extends GenericAction(value = 37)
|
||||||
final case object Unknown45 extends GenericAction(value = 45)
|
final case object Unknown45 extends GenericAction(value = 45)
|
||||||
|
|
||||||
final case class Unknown(override val value: Int) extends GenericAction(value)
|
final case class Unknown(override val value: Int) extends GenericAction(value)
|
||||||
}
|
}
|
||||||
|
|
@ -55,16 +55,19 @@ object GenericActionMessage extends Marshallable[GenericActionMessage] {
|
||||||
def apply(i: Int): GenericActionMessage = {
|
def apply(i: Int): GenericActionMessage = {
|
||||||
GenericActionMessage(GenericAction.values.find { _.value == i } match {
|
GenericActionMessage(GenericAction.values.find { _.value == i } match {
|
||||||
case Some(enum) => enum
|
case Some(enum) => enum
|
||||||
case None => GenericAction.Unknown(i)
|
case None => GenericAction.Unknown(i)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private val genericActionCodec = uint(bits = 6).xmap[GenericAction]({
|
private val genericActionCodec = uint(bits = 6).xmap[GenericAction](
|
||||||
i => GenericAction.values.find { _.value == i } match {
|
{ i =>
|
||||||
case Some(enum) => enum
|
GenericAction.values.find { _.value == i } match {
|
||||||
case None => GenericAction.Unknown(i)
|
case Some(enum) => enum
|
||||||
}
|
case None => GenericAction.Unknown(i)
|
||||||
}, enum => enum.value)
|
}
|
||||||
|
},
|
||||||
|
e => e.value
|
||||||
|
)
|
||||||
|
|
||||||
implicit val codec: Codec[GenericActionMessage] = ("action" | genericActionCodec).as[GenericActionMessage]
|
implicit val codec: Codec[GenericActionMessage] = ("action" | genericActionCodec).as[GenericActionMessage]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ object TerrainCondition extends Enumeration {
|
||||||
type Type = Value
|
type Type = Value
|
||||||
val Safe, Unsafe = Value
|
val Safe, Unsafe = Value
|
||||||
|
|
||||||
implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 1))
|
implicit val codec = PacketHelpers.createEnumerationCodec(e = this, uint(bits = 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,11 +28,11 @@ object TerrainCondition extends Enumeration {
|
||||||
* @param pos the vehicle's current position in the game world
|
* @param pos the vehicle's current position in the game world
|
||||||
*/
|
*/
|
||||||
final case class InvalidTerrainMessage(
|
final case class InvalidTerrainMessage(
|
||||||
player_guid: PlanetSideGUID,
|
player_guid: PlanetSideGUID,
|
||||||
vehicle_guid: PlanetSideGUID,
|
vehicle_guid: PlanetSideGUID,
|
||||||
proximity_alert: TerrainCondition.Value,
|
proximity_alert: TerrainCondition.Value,
|
||||||
pos: Vector3
|
pos: Vector3
|
||||||
) extends PlanetSideGamePacket {
|
) extends PlanetSideGamePacket {
|
||||||
type Packet = InvalidTerrainMessage
|
type Packet = InvalidTerrainMessage
|
||||||
def opcode = GamePacketOpcode.InvalidTerrainMessage
|
def opcode = GamePacketOpcode.InvalidTerrainMessage
|
||||||
def encode = InvalidTerrainMessage.encode(this)
|
def encode = InvalidTerrainMessage.encode(this)
|
||||||
|
|
@ -40,8 +40,7 @@ final case class InvalidTerrainMessage(
|
||||||
|
|
||||||
object InvalidTerrainMessage extends Marshallable[InvalidTerrainMessage] {
|
object InvalidTerrainMessage extends Marshallable[InvalidTerrainMessage] {
|
||||||
|
|
||||||
implicit val codec: Codec[InvalidTerrainMessage] = (
|
implicit val codec: Codec[InvalidTerrainMessage] = (("player_guid" | PlanetSideGUID.codec) ::
|
||||||
("player_guid" | PlanetSideGUID.codec) ::
|
|
||||||
("vehicle_guid" | PlanetSideGUID.codec) ::
|
("vehicle_guid" | PlanetSideGUID.codec) ::
|
||||||
("proximity_alert" | TerrainCondition.codec) ::
|
("proximity_alert" | TerrainCondition.codec) ::
|
||||||
("pos" | floatL :: floatL :: floatL).narrow[Vector3](
|
("pos" | floatL :: floatL :: floatL).narrow[Vector3](
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ object SquadAction {
|
||||||
|
|
||||||
val AnyPositions, AvailablePositions, SomeCertifications, AllCertifications = Value
|
val AnyPositions, AvailablePositions, SomeCertifications, AllCertifications = Value
|
||||||
|
|
||||||
implicit val codec: Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
|
implicit val codec: Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(e = this, uint(bits = 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class DisplaySquad() extends SquadAction(code = 0)
|
final case class DisplaySquad() extends SquadAction(code = 0)
|
||||||
|
|
@ -280,7 +280,7 @@ object SquadAction {
|
||||||
|
|
||||||
val squadListDecoratorCodec = (
|
val squadListDecoratorCodec = (
|
||||||
SquadListDecoration.codec ::
|
SquadListDecoration.codec ::
|
||||||
ignore(size = 3)
|
ignore(size = 3)
|
||||||
).xmap[SquadListDecorator](
|
).xmap[SquadListDecorator](
|
||||||
{
|
{
|
||||||
case value :: _ :: HNil => SquadListDecorator(value)
|
case value :: _ :: HNil => SquadListDecorator(value)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ object MemberEvent extends Enumeration {
|
||||||
|
|
||||||
val Add, Remove, Promote, UpdateZone, Outfit = Value
|
val Add, Remove, Promote, UpdateZone, Outfit = Value
|
||||||
|
|
||||||
implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
|
implicit val codec = PacketHelpers.createEnumerationCodec(e = this, uint(bits = 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class SquadMemberEvent(
|
final case class SquadMemberEvent(
|
||||||
|
|
@ -58,13 +58,18 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
|
||||||
("unk2" | uint16L) ::
|
("unk2" | uint16L) ::
|
||||||
("char_id" | uint32L) ::
|
("char_id" | uint32L) ::
|
||||||
("position" | uint4) ::
|
("position" | uint4) ::
|
||||||
("player_name" | conditional(action == MemberEvent.Add, PacketHelpers.encodedWideStringAligned(adjustment = 1))) ::
|
("player_name" | conditional(
|
||||||
|
action == MemberEvent.Add,
|
||||||
|
PacketHelpers.encodedWideStringAligned(adjustment = 1)
|
||||||
|
)) ::
|
||||||
("zone_number" | conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, uint16L)) ::
|
("zone_number" | conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, uint16L)) ::
|
||||||
("outfit_id" | conditional(action == MemberEvent.Add || action == MemberEvent.Outfit, uint32L))
|
("outfit_id" | conditional(action == MemberEvent.Add || action == MemberEvent.Outfit, uint32L))
|
||||||
}).exmap[SquadMemberEvent](
|
}).exmap[SquadMemberEvent](
|
||||||
{
|
{
|
||||||
case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: outfit_id :: HNil =>
|
case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: outfit_id :: HNil =>
|
||||||
Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, outfit_id))
|
Attempt.Successful(
|
||||||
|
SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, outfit_id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case SquadMemberEvent(
|
case SquadMemberEvent(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ object WaypointEventAction extends Enumeration {
|
||||||
val Add, Unknown1, Remove, Unknown3 //unconfirmed
|
val Add, Unknown1, Remove, Unknown3 //unconfirmed
|
||||||
= Value
|
= Value
|
||||||
|
|
||||||
implicit val codec: Codec[WaypointEventAction.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint2)
|
implicit val codec: Codec[WaypointEventAction.Value] = PacketHelpers.createEnumerationCodec(e = this, uint2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -332,4 +332,3 @@ object MountableInventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
package net.psforever.services
|
package net.psforever.services
|
||||||
|
|
||||||
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
|
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
|
||||||
|
|
@ -7,7 +8,7 @@ import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.Avatar
|
import net.psforever.objects.avatar.Avatar
|
||||||
import net.psforever.objects.{Player, SpawnPoint, Vehicle}
|
import net.psforever.objects.{Player, SpawnPoint, Vehicle}
|
||||||
import net.psforever.objects.serverobject.structures.{Building, WarpGate}
|
import net.psforever.objects.serverobject.structures.{Building, WarpGate}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.{HotSpotInfo, Zone}
|
||||||
import net.psforever.packet.game.DroppodError
|
import net.psforever.packet.game.DroppodError
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
|
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.Config
|
||||||
|
|
@ -101,8 +102,8 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
import InterstellarClusterService._
|
import InterstellarClusterService._
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
var intercontinentalSetup: Boolean = false
|
private var intercontinentalSetup: Boolean = false
|
||||||
var cavernRotation: Option[ActorRef[CavernRotationService.Command]] = None
|
private var cavernRotation: Option[ActorRef[CavernRotationService.Command]] = None
|
||||||
|
|
||||||
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = {
|
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = {
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
@ -164,47 +165,44 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
|
||||||
replyTo ! ZonesResponse(zones.filter(predicate))
|
replyTo ! ZonesResponse(zones.filter(predicate))
|
||||||
|
|
||||||
case GetInstantActionSpawnPoint(faction, replyTo) =>
|
case GetInstantActionSpawnPoint(faction, replyTo) =>
|
||||||
val res = zones
|
val spawnTarget: Seq[SpawnGroup] = if (Config.app.game.instantAction.spawnOnAms) {
|
||||||
.filter(_.Players.nonEmpty)
|
Seq(SpawnGroup.Tower, SpawnGroup.Facility, SpawnGroup.AMS)
|
||||||
.flatMap { zone =>
|
} else {
|
||||||
zone.HotSpotData.collect {
|
Seq(SpawnGroup.Tower, SpawnGroup.Facility)
|
||||||
case spot => (zone, spot)
|
}
|
||||||
|
val hotspotsAndSpawnsInZones: Iterable[(Zone, HotSpotInfo, List[SpawnPoint])] = zones
|
||||||
|
.collect {
|
||||||
|
case zone if !zone.map.cavern && zone.Players.nonEmpty =>
|
||||||
|
zone.HotSpotData
|
||||||
|
.map {
|
||||||
|
info => (zone, info, zone.findNearestSpawnPoints(faction, info.DisplayLocation, spawnTarget))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatten
|
||||||
|
.collect {
|
||||||
|
case (zone, info, Some(spawns)) if spawns.nonEmpty => (zone, info, spawns)
|
||||||
|
}
|
||||||
|
val spawnResults: Option[(Zone, SpawnPoint)] = hotspotsAndSpawnsInZones
|
||||||
|
.maxByOption {
|
||||||
|
case (_, info, _) => info.ActivityFor(faction).map(_.Heat).getOrElse(0) //heat by target faction
|
||||||
|
}
|
||||||
|
.orElse {
|
||||||
|
if (Config.app.game.instantAction.thirdParty) {
|
||||||
|
hotspotsAndSpawnsInZones
|
||||||
|
.maxByOption {
|
||||||
|
case (_, info, _) => info.Activity.values.foldLeft(0)(_ + _.Heat) //sum of heat for all factions
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
case (zone, spot) =>
|
|
||||||
(
|
|
||||||
zone,
|
|
||||||
spot,
|
|
||||||
zone.findNearestSpawnPoints(
|
|
||||||
faction,
|
|
||||||
spot.DisplayLocation,
|
|
||||||
if (Config.app.game.instantActionAms) Seq(SpawnGroup.Tower, SpawnGroup.Facility, SpawnGroup.AMS)
|
|
||||||
else Seq(SpawnGroup.Tower, SpawnGroup.Facility)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.collect {
|
|
||||||
case (zone, info, Some(spawns)) => (zone, info, spawns)
|
|
||||||
}
|
|
||||||
.toList
|
|
||||||
.sortBy { case (_, spot, _) => spot.Activity.values.foldLeft(0)(_ + _.Heat) }(
|
|
||||||
Ordering[Int].reverse
|
|
||||||
) // greatest > least
|
|
||||||
.sortWith {
|
|
||||||
case ((_, spot1, _), (_, _, _)) =>
|
|
||||||
spot1.ActivityBy().contains(faction) // prefer own faction activity
|
|
||||||
}
|
|
||||||
.headOption
|
|
||||||
.flatMap {
|
|
||||||
case (zone, info, spawns) =>
|
case (zone, info, spawns) =>
|
||||||
val pos = info.DisplayLocation
|
val pos = info.DisplayLocation
|
||||||
val spawnPoint = spawns.minBy(point => Vector3.DistanceSquared(point.Position, pos))
|
val spawnPoint = spawns.minBy(point => Vector3.DistanceSquared(point.Position, pos))
|
||||||
//Some(zone, pos, spawnPoint)
|
(zone, spawnPoint)
|
||||||
Some(zone, spawnPoint)
|
|
||||||
case _ => None
|
|
||||||
}
|
}
|
||||||
replyTo ! SpawnPointResponse(res)
|
replyTo ! SpawnPointResponse(spawnResults)
|
||||||
|
|
||||||
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
|
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
|
||||||
val response = zones.find(_.Number == zoneNumber) match {
|
val response = zones.find(_.Number == zoneNumber) match {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
package net.psforever.services.local.support
|
package net.psforever.services.local.support
|
||||||
|
|
||||||
import akka.actor.{Actor, Cancellable}
|
import akka.actor.{Actor, Cancellable}
|
||||||
import net.psforever.objects.{Default, Player}
|
import net.psforever.objects.{Default, Doors}
|
||||||
import net.psforever.objects.serverobject.doors.Door
|
import net.psforever.objects.serverobject.doors.Door
|
||||||
import net.psforever.objects.serverobject.structures.Building
|
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
import net.psforever.types.PlanetSideGUID
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -17,7 +16,6 @@ import scala.concurrent.duration._
|
||||||
* @see `LocalService`
|
* @see `LocalService`
|
||||||
*/
|
*/
|
||||||
class DoorCloseActor() extends Actor {
|
class DoorCloseActor() extends Actor {
|
||||||
|
|
||||||
/** The periodic `Executor` that checks for doors to be closed */
|
/** The periodic `Executor` that checks for doors to be closed */
|
||||||
private var doorCloserTrigger: Cancellable = Default.Cancellable
|
private var doorCloserTrigger: Cancellable = Default.Cancellable
|
||||||
|
|
||||||
|
|
@ -36,43 +34,11 @@ class DoorCloseActor() extends Actor {
|
||||||
|
|
||||||
case DoorCloseActor.TryCloseDoors() =>
|
case DoorCloseActor.TryCloseDoors() =>
|
||||||
doorCloserTrigger.cancel()
|
doorCloserTrigger.cancel()
|
||||||
val now: Long = System.nanoTime
|
val now: Long = System.currentTimeMillis()
|
||||||
val (doorsToClose1, doorsLeftOpen1) = PartitionEntries(openDoors, now)
|
val (doorsToClose1, doorsLeftOpen1) = PartitionEntries(openDoors, now)
|
||||||
val (doorsToClose2, doorsLeftOpen2) = doorsToClose1.partition(entry => {
|
val (doorsToClose2, doorsLeftOpen2) = doorsToClose1.partition { entry =>
|
||||||
entry.door.Open match {
|
Doors.testForTargetHoldingDoorOpen(entry.door, entry.door.Definition.continuousOpenDistance).isEmpty
|
||||||
case Some(player) =>
|
}
|
||||||
// If the player that opened the door is far enough away, or they're dead,
|
|
||||||
var openerIsGone = Vector3.MagnitudeSquared(entry.door.Position - player.Position) > 25.5 || !player.isAlive
|
|
||||||
|
|
||||||
if (openerIsGone) {
|
|
||||||
// Check nobody else is nearby to hold the door opens
|
|
||||||
val playersToCheck: List[Player] =
|
|
||||||
if (
|
|
||||||
entry.door.Owner
|
|
||||||
.isInstanceOf[Building] && entry.door.Owner.asInstanceOf[Building].Definition.SOIRadius > 0
|
|
||||||
) {
|
|
||||||
entry.door.Owner.asInstanceOf[Building].PlayersInSOI
|
|
||||||
} else {
|
|
||||||
entry.zone.LivePlayers
|
|
||||||
}
|
|
||||||
|
|
||||||
playersToCheck
|
|
||||||
.filter(x => x.isAlive && Vector3.MagnitudeSquared(entry.door.Position - x.Position) < 25.5)
|
|
||||||
.headOption match {
|
|
||||||
case Some(newOpener) =>
|
|
||||||
// Another player is near the door, keep it open
|
|
||||||
entry.door.Open = newOpener
|
|
||||||
openerIsGone = false
|
|
||||||
case _ => ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openerIsGone
|
|
||||||
case None =>
|
|
||||||
// Door should not be open. Mark it to be closed.
|
|
||||||
true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
openDoors = (
|
openDoors = (
|
||||||
doorsLeftOpen1 ++
|
doorsLeftOpen1 ++
|
||||||
doorsLeftOpen2.map(entry => DoorCloseActor.DoorEntry(entry.door, entry.zone, now))
|
doorsLeftOpen2.map(entry => DoorCloseActor.DoorEntry(entry.door, entry.zone, now))
|
||||||
|
|
@ -89,7 +55,7 @@ class DoorCloseActor() extends Actor {
|
||||||
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
|
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
|
||||||
}
|
}
|
||||||
|
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,9 +110,9 @@ class DoorCloseActor() extends Actor {
|
||||||
object DoorCloseActor {
|
object DoorCloseActor {
|
||||||
|
|
||||||
/** The wait before an open door closes; as a Long for calculation simplicity */
|
/** The wait before an open door closes; as a Long for calculation simplicity */
|
||||||
private final val timeout_time: Long = 5000000000L //nanoseconds (5s)
|
private final val timeout_time: Long = 5000L //milliseconds (5s)
|
||||||
/** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */
|
/** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */
|
||||||
private final val timeout: FiniteDuration = timeout_time nanoseconds
|
private final val timeout: FiniteDuration = timeout_time milliseconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message that carries information about a door that has been opened.
|
* Message that carries information about a door that has been opened.
|
||||||
|
|
@ -155,7 +121,7 @@ object DoorCloseActor {
|
||||||
* @param time when the door was opened
|
* @param time when the door was opened
|
||||||
* @see `DoorEntry`
|
* @see `DoorEntry`
|
||||||
*/
|
*/
|
||||||
final case class DoorIsOpen(door: Door, zone: Zone, time: Long = System.nanoTime())
|
final case class DoorIsOpen(door: Door, zone: Zone, time: Long = System.currentTimeMillis())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message that carries information about a door that needs to close.
|
* Message that carries information about a door that needs to close.
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import scodec.codecs.uint2L
|
||||||
* Blame the lack of gender dysphoria on the Terran Republic.
|
* Blame the lack of gender dysphoria on the Terran Republic.
|
||||||
*/
|
*/
|
||||||
sealed abstract class CharacterSex(
|
sealed abstract class CharacterSex(
|
||||||
val value: Int,
|
val value: Int,
|
||||||
val pronounSubject: String,
|
val pronounSubject: String,
|
||||||
val pronounObject: String,
|
val pronounObject: String,
|
||||||
val possessive: String
|
val possessive: String
|
||||||
) extends IntEnumEntry {
|
) extends IntEnumEntry {
|
||||||
def possessiveNoObject: String = possessive
|
def possessiveNoObject: String = possessive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,21 +25,23 @@ sealed abstract class CharacterSex(
|
||||||
object CharacterSex extends IntEnum[CharacterSex] {
|
object CharacterSex extends IntEnum[CharacterSex] {
|
||||||
val values = findValues
|
val values = findValues
|
||||||
|
|
||||||
case object Male extends CharacterSex(
|
case object Male
|
||||||
value = 1,
|
extends CharacterSex(
|
||||||
pronounSubject = "he",
|
value = 1,
|
||||||
pronounObject = "him",
|
pronounSubject = "he",
|
||||||
possessive = "his"
|
pronounObject = "him",
|
||||||
)
|
possessive = "his"
|
||||||
|
)
|
||||||
|
|
||||||
case object Female extends CharacterSex(
|
case object Female
|
||||||
value = 2,
|
extends CharacterSex(
|
||||||
pronounSubject = "she",
|
value = 2,
|
||||||
pronounObject = "her",
|
pronounSubject = "she",
|
||||||
possessive = "her"
|
pronounObject = "her",
|
||||||
) {
|
possessive = "her"
|
||||||
|
) {
|
||||||
override def possessiveNoObject: String = "hers"
|
override def possessiveNoObject: String = "hers"
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit val codec = PacketHelpers.createIntEnumCodec(enum = this, uint2L)
|
implicit val codec = PacketHelpers.createIntEnumCodec(e = this, uint2L)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ sealed abstract class ExperienceType(val value: Int) extends IntEnumEntry
|
||||||
object ExperienceType extends IntEnum[ExperienceType] {
|
object ExperienceType extends IntEnum[ExperienceType] {
|
||||||
val values: IndexedSeq[ExperienceType] = findValues
|
val values: IndexedSeq[ExperienceType] = findValues
|
||||||
|
|
||||||
case object Normal extends ExperienceType(value = 0)
|
case object Normal extends ExperienceType(value = 0)
|
||||||
case object Support extends ExperienceType(value = 2)
|
case object Support extends ExperienceType(value = 2)
|
||||||
case object RabbitBall extends ExperienceType(value = 4)
|
case object RabbitBall extends ExperienceType(value = 4)
|
||||||
|
|
||||||
implicit val codec: Codec[ExperienceType] = PacketHelpers.createIntEnumCodec(enum = this, uint(bits = 3))
|
implicit val codec: Codec[ExperienceType] = PacketHelpers.createIntEnumCodec(e = this, uint(bits = 3))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,8 @@ object MeritCommendation extends Enumeration {
|
||||||
{
|
{
|
||||||
case MeritCommendation.None =>
|
case MeritCommendation.None =>
|
||||||
Attempt.successful(0xffffffffL)
|
Attempt.successful(0xffffffffL)
|
||||||
case enum =>
|
case e =>
|
||||||
Attempt.successful(enum.id.toLong)
|
Attempt.successful(e.id.toLong)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,5 @@ object OxygenState extends Enum[OxygenState] {
|
||||||
case object Recovery extends OxygenState
|
case object Recovery extends OxygenState
|
||||||
case object Suffocation extends OxygenState
|
case object Suffocation extends OxygenState
|
||||||
|
|
||||||
implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(enum = this, uint(bits = 1))
|
implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(e = this, uint(bits = 1))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
package net.psforever.types
|
package net.psforever.types
|
||||||
|
|
||||||
import enumeratum.values.{IntEnum, IntEnumEntry}
|
import enumeratum.values.{IntEnum, IntEnumEntry}
|
||||||
import net.psforever.types.StatisticalElement.{AMS, ANT, AgileExoSuit, ApcNc, ApcTr, ApcVs, Aphelion, AphelionFlight, AphelionGunner, Battlewagon, Colossus, ColossusFlight, ColossusGunner, Droppod, Dropship, Flail, Fury, GalaxyGunship, ImplantTerminalMech, InfiltrationExoSuit, Liberator, Lightgunship, Lightning, Lodestar, Magrider, MechanizedAssaultExoSuit, MediumTransport, Mosquito, Peregrine, PeregrineFlight, PeregrineGunner, PhalanxTurret, Phantasm, PortableMannedTurretNc, PortableMannedTurretTr, PortableMannedTurretVs, Prowler, QuadAssault, QuadStealth, Raider, ReinforcedExoSuit, Router, Skyguard, SpitfireAA, SpitfireCloaked, SpitfireTurret, StandardExoSuit, Sunderer, Switchblade, TankTraps, ThreeManHeavyBuggy, Thunderer, TwoManAssaultBuggy, TwoManHeavyBuggy, TwoManHoverBuggy, VanSentryTurret, Vanguard, Vulture, Wasp}
|
import net.psforever.types.StatisticalElement.{AMS, ANT, AgileExoSuit, ApcNc, ApcTr, ApcVs, Aphelion, AphelionFlight, AphelionGunner, Battlewagon, Colossus, ColossusFlight, ColossusGunner, Dropship, Flail, Fury, GalaxyGunship, InfiltrationExoSuit, Liberator, Lightgunship, Lightning, Lodestar, Magrider, MechanizedAssaultExoSuit, MediumTransport, Mosquito, Peregrine, PeregrineFlight, PeregrineGunner, PhalanxTurret, PortableMannedTurretNc, PortableMannedTurretTr, PortableMannedTurretVs, Prowler, QuadAssault, QuadStealth, Raider, ReinforcedExoSuit, Router, Skyguard, StandardExoSuit, Sunderer, Switchblade, ThreeManHeavyBuggy, Thunderer, TwoManAssaultBuggy, TwoManHeavyBuggy, TwoManHoverBuggy, VanSentryTurret, Vanguard, Vulture, Wasp}
|
||||||
|
|
||||||
sealed abstract class StatisticalCategory(val value: Int) extends IntEnumEntry
|
sealed abstract class StatisticalCategory(val value: Int) extends IntEnumEntry
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ object Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit
|
implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit
|
||||||
enum: IntEnum[A],
|
e: IntEnum[A],
|
||||||
ct: ClassTag[A]
|
ct: ClassTag[A]
|
||||||
): ConfigConvert[A] =
|
): ConfigConvert[A] =
|
||||||
viaNonEmptyStringOpt[A](
|
viaNonEmptyStringOpt[A](
|
||||||
v =>
|
v =>
|
||||||
enum.values.toList.collectFirst {
|
e.values.toList.collectFirst {
|
||||||
case e: ServerType if e.name == v => e.asInstanceOf[A]
|
case e: ServerType if e.name == v => e.asInstanceOf[A]
|
||||||
case e: BattleRank if e.value.toString == v => e.asInstanceOf[A]
|
case e: BattleRank if e.value.toString == v => e.asInstanceOf[A]
|
||||||
case e: CommandRank if e.value.toString == v => e.asInstanceOf[A]
|
case e: CommandRank if e.value.toString == v => e.asInstanceOf[A]
|
||||||
|
|
@ -40,13 +40,13 @@ object Config {
|
||||||
)
|
)
|
||||||
|
|
||||||
implicit def enumeratumConfigConvert[A <: EnumEntry](implicit
|
implicit def enumeratumConfigConvert[A <: EnumEntry](implicit
|
||||||
enum: Enum[A],
|
e: Enum[A],
|
||||||
ct: ClassTag[A]
|
ct: ClassTag[A]
|
||||||
): ConfigConvert[A] =
|
): ConfigConvert[A] =
|
||||||
viaNonEmptyStringOpt[A](
|
viaNonEmptyStringOpt[A](
|
||||||
v =>
|
v =>
|
||||||
enum.values.toList.collectFirst {
|
e.values.toList.collectFirst {
|
||||||
case e if e.toString.toLowerCase == v.toLowerCase => e.asInstanceOf[A]
|
case e if e.toString.toLowerCase == v.toLowerCase => e
|
||||||
},
|
},
|
||||||
_.toString
|
_.toString
|
||||||
)
|
)
|
||||||
|
|
@ -62,25 +62,23 @@ object Config {
|
||||||
// Raw config object - prefer app when possible
|
// Raw config object - prefer app when possible
|
||||||
lazy val config: TypesafeConfig = source.config() match {
|
lazy val config: TypesafeConfig = source.config() match {
|
||||||
case Right(config) => config
|
case Right(config) => config
|
||||||
case Left(failures) => {
|
case Left(failures) =>
|
||||||
logger.error("Loading config failed")
|
logger.error("Loading config failed")
|
||||||
failures.toList.foreach { failure =>
|
failures.toList.foreach { failure =>
|
||||||
logger.error(failure.toString)
|
logger.error(failure.toString)
|
||||||
}
|
}
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typed config object
|
// Typed config object
|
||||||
lazy val app: AppConfig = source.load[AppConfig] match {
|
lazy val app: AppConfig = source.load[AppConfig] match {
|
||||||
case Right(config) => config
|
case Right(config) => config
|
||||||
case Left(failures) => {
|
case Left(failures) =>
|
||||||
logger.error("Loading config failed")
|
logger.error("Loading config failed")
|
||||||
failures.toList.foreach { failure =>
|
failures.toList.foreach { failure =>
|
||||||
logger.error(failure.toString)
|
logger.error(failure.toString)
|
||||||
}
|
}
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +146,7 @@ case class SessionConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
case class GameConfig(
|
case class GameConfig(
|
||||||
instantActionAms: Boolean,
|
instantAction: InstantActionConfig,
|
||||||
amenityAutorepairRate: Float,
|
amenityAutorepairRate: Float,
|
||||||
amenityAutorepairDrainRate: Float,
|
amenityAutorepairDrainRate: Float,
|
||||||
bepRate: Double,
|
bepRate: Double,
|
||||||
|
|
@ -161,6 +159,12 @@ case class GameConfig(
|
||||||
cavernRotation: CavernRotationConfig,
|
cavernRotation: CavernRotationConfig,
|
||||||
savedMsg: SavedMessageEvents,
|
savedMsg: SavedMessageEvents,
|
||||||
playerDraw: PlayerStateDrawSettings
|
playerDraw: PlayerStateDrawSettings
|
||||||
|
doorsCanBeOpenedByMedAppFromThisDistance: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
case class InstantActionConfig(
|
||||||
|
spawnOnAms: Boolean,
|
||||||
|
thirdParty: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
case class NewAvatar(
|
case class NewAvatar(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
|
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
|
||||||
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
||||||
import net.psforever.objects.sourcing.{DeployableSource, ObjectSource, PlayerSource, TurretSource, VehicleSource}
|
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, TurretSource, VehicleSource}
|
||||||
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
|
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
|
||||||
import net.psforever.types.{Angular, PlanetSideEmpire, Vector3}
|
import net.psforever.types.{Angular, PlanetSideEmpire, Vector3}
|
||||||
import net.psforever.util.DefinitionUtil
|
import net.psforever.util.DefinitionUtil
|
||||||
|
|
|
||||||
|
|
@ -45,4 +45,3 @@ class CaptureFlagUpdateMessageTest extends Specification with Debug {
|
||||||
pkt mustEqual stringOne
|
pkt mustEqual stringOne
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,3 @@ class ComponentDamageMessageTest extends Specification {
|
||||||
pkt mustEqual string_off
|
pkt mustEqual string_off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,3 @@ class FrameVehicleStateMessageTest extends Specification {
|
||||||
pkt mustEqual string
|
pkt mustEqual string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,4 +28,3 @@ class GenericObjectActionAtPositionMessageTest extends Specification {
|
||||||
pkt mustEqual string
|
pkt mustEqual string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,4 +347,3 @@ class BattleframeRoboticsTest extends Specification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package objects
|
package objects
|
||||||
|
|
||||||
import akka.actor.{ActorSystem, Props}
|
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||||
import akka.testkit.TestProbe
|
import akka.testkit.TestProbe
|
||||||
import base.ActorTest
|
import base.ActorTest
|
||||||
import net.psforever.objects.avatar.Avatar
|
import net.psforever.objects.avatar.Avatar
|
||||||
|
|
@ -12,15 +12,14 @@ import net.psforever.objects.{Default, GlobalDefinitions, Player}
|
||||||
import net.psforever.objects.serverobject.doors.{Door, DoorControl}
|
import net.psforever.objects.serverobject.doors.{Door, DoorControl}
|
||||||
import net.psforever.objects.serverobject.structures.{Building, StructureType}
|
import net.psforever.objects.serverobject.structures.{Building, StructureType}
|
||||||
import net.psforever.objects.zones.{Zone, ZoneMap}
|
import net.psforever.objects.zones.{Zone, ZoneMap}
|
||||||
import net.psforever.packet.game.UseItemMessage
|
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
|
||||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
|
||||||
import net.psforever.types._
|
import net.psforever.types._
|
||||||
import org.specs2.mutable.Specification
|
import org.specs2.mutable.Specification
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
class DoorTest extends Specification {
|
class DoorTest extends Specification {
|
||||||
val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
private val player: Player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||||
|
|
||||||
"Door" should {
|
"Door" should {
|
||||||
"construct" in {
|
"construct" in {
|
||||||
|
|
@ -49,19 +48,6 @@ class DoorTest extends Specification {
|
||||||
}
|
}
|
||||||
|
|
||||||
"be opened and closed (2; toggle)" in {
|
"be opened and closed (2; toggle)" in {
|
||||||
val msg = UseItemMessage(
|
|
||||||
PlanetSideGUID(6585),
|
|
||||||
PlanetSideGUID(0),
|
|
||||||
PlanetSideGUID(372),
|
|
||||||
4294967295L,
|
|
||||||
false,
|
|
||||||
Vector3(5.0f, 0.0f, 0.0f),
|
|
||||||
Vector3(0.0f, 0.0f, 0.0f),
|
|
||||||
11,
|
|
||||||
25,
|
|
||||||
0,
|
|
||||||
364
|
|
||||||
)
|
|
||||||
val door = Door(GlobalDefinitions.door)
|
val door = Door(GlobalDefinitions.door)
|
||||||
door.Open.isEmpty mustEqual true
|
door.Open.isEmpty mustEqual true
|
||||||
door.Open = player
|
door.Open = player
|
||||||
|
|
@ -74,7 +60,7 @@ class DoorTest extends Specification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DoorControl1Test extends ActorTest {
|
class DoorControlConstructTest extends ActorTest {
|
||||||
"DoorControl" should {
|
"DoorControl" should {
|
||||||
"construct" in {
|
"construct" in {
|
||||||
val door = Door(GlobalDefinitions.door)
|
val door = Door(GlobalDefinitions.door)
|
||||||
|
|
@ -84,28 +70,11 @@ class DoorControl1Test extends ActorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DoorControl2Test extends ActorTest {
|
class DoorControlOpenTest extends ActorTest {
|
||||||
"DoorControl" should {
|
"DoorControl" should {
|
||||||
"open on use" in {
|
"open on use" in {
|
||||||
val (player, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
||||||
val probe = new TestProbe(system)
|
door.Actor ! CommonMessages.Use(player)
|
||||||
door.Zone.LocalEvents = probe.ref
|
|
||||||
val msg = UseItemMessage(
|
|
||||||
PlanetSideGUID(1),
|
|
||||||
PlanetSideGUID(0),
|
|
||||||
PlanetSideGUID(2),
|
|
||||||
0L,
|
|
||||||
false,
|
|
||||||
Vector3(0f, 0f, 0f),
|
|
||||||
Vector3(0f, 0f, 0f),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0L
|
|
||||||
) //faked
|
|
||||||
assert(door.Open.isEmpty)
|
|
||||||
|
|
||||||
door.Actor ! CommonMessages.Use(player, Some(msg))
|
|
||||||
val reply = probe.receiveOne(1000 milliseconds)
|
val reply = probe.receiveOne(1000 milliseconds)
|
||||||
assert(reply match {
|
assert(reply match {
|
||||||
case LocalServiceMessage("test", LocalAction.DoorOpens(PlanetSideGUID(0), _, d)) => d eq door
|
case LocalServiceMessage("test", LocalAction.DoorOpens(PlanetSideGUID(0), _, d)) => d eq door
|
||||||
|
|
@ -116,26 +85,55 @@ class DoorControl2Test extends ActorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DoorControl3Test extends ActorTest {
|
class DoorControlTooFarTest extends ActorTest {
|
||||||
"DoorControl" should {
|
"DoorControl" should {
|
||||||
"do nothing if given garbage" in {
|
"do not open if the player is too far away" in {
|
||||||
val (_, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
||||||
|
player.Position = Vector3(10,0,0)
|
||||||
|
door.Actor ! CommonMessages.Use(player)
|
||||||
|
probe.expectNoMessage(Duration.create(500, "ms"))
|
||||||
|
assert(door.Open.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoorControlAlreadyOpenTest extends ActorTest {
|
||||||
|
"DoorControl" should {
|
||||||
|
"is already open" in {
|
||||||
|
val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
||||||
|
door.Open = player //door thinks it is open
|
||||||
|
door.Actor.tell(CommonMessages.Use(player), probe.ref)
|
||||||
|
val reply = probe.receiveOne(1000 milliseconds)
|
||||||
|
assert(reply match {
|
||||||
|
case LocalServiceResponse("test", _, LocalResponse.DoorOpens(guid)) => guid == door.GUID
|
||||||
|
case _ => false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoorControlGarbageDataTest extends ActorTest {
|
||||||
|
"DoorControl" should {
|
||||||
|
"do nothing if given garbage data" in {
|
||||||
|
val (_, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR)
|
||||||
assert(door.Open.isEmpty)
|
assert(door.Open.isEmpty)
|
||||||
|
|
||||||
door.Actor ! "trash"
|
door.Actor ! "trash"
|
||||||
expectNoMessage(Duration.create(500, "ms"))
|
probe.expectNoMessage(Duration.create(500, "ms"))
|
||||||
assert(door.Open.isEmpty)
|
assert(door.Open.isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoorControlTest {
|
object DoorControlTest {
|
||||||
def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door) = {
|
def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door, TestProbe) = {
|
||||||
|
val eventsProbe = new TestProbe(system)
|
||||||
val door = Door(GlobalDefinitions.door)
|
val door = Door(GlobalDefinitions.door)
|
||||||
val guid = new NumberPoolHub(new MaxNumberSource(5))
|
val guid = new NumberPoolHub(new MaxNumberSource(5))
|
||||||
val zone = new Zone("test", new ZoneMap("test"), 0) {
|
val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) {
|
||||||
override def SetupNumberPools() = {}
|
override def SetupNumberPools(): Unit = {}
|
||||||
GUID(guid)
|
GUID(guid)
|
||||||
|
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||||
}
|
}
|
||||||
guid.register(door, 1)
|
guid.register(door, 1)
|
||||||
door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door")
|
door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door")
|
||||||
|
|
@ -149,7 +147,8 @@ object DoorControlTest {
|
||||||
)
|
)
|
||||||
door.Owner.Faction = faction
|
door.Owner.Faction = faction
|
||||||
val player = Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute))
|
val player = Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||||
|
player.Zone = zone
|
||||||
guid.register(player, 2)
|
guid.register(player, 2)
|
||||||
(player, door)
|
(player, door, eventsProbe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor
|
||||||
lodestar.Actor ! Vehicle.Deconstruct()
|
lodestar.Actor ! Vehicle.Deconstruct()
|
||||||
|
|
||||||
val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds)
|
val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds)
|
||||||
vehicle_msg(5) match {
|
vehicle_msg.head match {
|
||||||
case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, true, PlanetSideGUID(2))) => ;
|
case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, true, PlanetSideGUID(2))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-1: ${vehicle_msg(5)}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-1: ${vehicle_msg(5)}")
|
||||||
|
|
@ -226,27 +226,27 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor
|
||||||
assert(player2.VehicleSeated.isEmpty)
|
assert(player2.VehicleSeated.isEmpty)
|
||||||
assert(lodestar.Seats(0).occupant.isEmpty)
|
assert(lodestar.Seats(0).occupant.isEmpty)
|
||||||
//cargo dismounting messages
|
//cargo dismounting messages
|
||||||
vehicle_msg.head match {
|
vehicle_msg(1) match {
|
||||||
case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => ;
|
case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-2: ${vehicle_msg.head}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-2: ${vehicle_msg.head}")
|
||||||
}
|
}
|
||||||
vehicle_msg(1) match {
|
vehicle_msg(2) match {
|
||||||
case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => ;
|
case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-3: ${vehicle_msg(1)}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-3: ${vehicle_msg(1)}")
|
||||||
}
|
}
|
||||||
vehicle_msg(2) match {
|
vehicle_msg(3) match {
|
||||||
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0))) => ;
|
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-4: ${vehicle_msg(2)}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-4: ${vehicle_msg(2)}")
|
||||||
}
|
}
|
||||||
vehicle_msg(3) match {
|
vehicle_msg(4) match {
|
||||||
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => ;
|
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-5: ${vehicle_msg(3)}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-5: ${vehicle_msg(3)}")
|
||||||
}
|
}
|
||||||
vehicle_msg(4) match {
|
vehicle_msg(5) match {
|
||||||
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => ;
|
case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => ;
|
||||||
case _ =>
|
case _ =>
|
||||||
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-6: ${vehicle_msg(4)}")
|
assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-6: ${vehicle_msg(4)}")
|
||||||
|
|
|
||||||
|
|
@ -48,30 +48,52 @@ import scala.collection.mutable
|
||||||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
import java.util.concurrent.{Executors, TimeUnit}
|
import java.util.concurrent.{Executors, TimeUnit}
|
||||||
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.types._
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.concurrent.Await
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
|
|
||||||
object Client {
|
object Client {
|
||||||
|
implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
|
||||||
Security.addProvider(new BouncyCastleProvider)
|
Security.addProvider(new BouncyCastleProvider)
|
||||||
|
|
||||||
private[this] val log = org.log4s.getLogger
|
private[this] val log = org.log4s.getLogger
|
||||||
|
|
||||||
def main(args: Array[String]): Unit = {
|
def main(args: Array[String]): Unit = {
|
||||||
val client = new Client("test", "test")
|
|
||||||
client.login(new InetSocketAddress("localhost", 51000))
|
|
||||||
client.joinWorld(client.state.worlds.head)
|
|
||||||
client.selectCharacter(client.state.characters.head.charId)
|
|
||||||
client.startTasks()
|
|
||||||
|
|
||||||
while (true) {
|
for (i <- 0 until 20) {
|
||||||
client.updateAvatar(client.state.avatar.copy(crouching = !client.state.avatar.crouching))
|
val id = i
|
||||||
Thread.sleep(2000)
|
|
||||||
//Thread.sleep(Int.MaxValue)
|
new Thread {
|
||||||
|
override def run: Unit = {
|
||||||
|
val client = new Client(s"bot${id}", "bot")
|
||||||
|
client.login(new InetSocketAddress("localhost", 51000))
|
||||||
|
|
||||||
|
client.joinWorld(client.state.worlds.head)
|
||||||
|
|
||||||
|
if (client.state.characters.isEmpty) {
|
||||||
|
client.createCharacter(s"bot${id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
client.selectCharacter(client.state.characters.head.charId)
|
||||||
|
client.startTasks()
|
||||||
|
|
||||||
|
client.send(ChatMsg(ChatMessageType.CMT_ZONE, wideContents = false, "", "z1", None))
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
client.updateAvatar(client.state.avatar.copy(crouching = !client.state.avatar.crouching))
|
||||||
|
Thread.sleep(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Await.ready(Future.never, Duration.Inf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Client(username: String, password: String) {
|
class Client(username: String, password: String) {
|
||||||
import Client._
|
|
||||||
|
|
||||||
private var sequence = 0
|
private var sequence = 0
|
||||||
private def nextSequence = {
|
private def nextSequence = {
|
||||||
val r = sequence
|
val r = sequence
|
||||||
|
|
@ -188,21 +210,34 @@ class Client(username: String, password: String) {
|
||||||
}
|
}
|
||||||
setupConnection()
|
setupConnection()
|
||||||
send(ConnectToWorldRequestMessage("", state.token.get, 0, 0, 0, "", 0)).require
|
send(ConnectToWorldRequestMessage("", state.token.get, 0, 0, 0, "", 0)).require
|
||||||
waitFor[CharacterInfoMessage]().require
|
while (true) {
|
||||||
|
val r = waitFor[CharacterInfoMessage]().require
|
||||||
|
if (r.finished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def selectCharacter(charId: Long): Unit = {
|
def selectCharacter(charId: Long): Unit = {
|
||||||
assert(state.connection == Connection.AvatarSelection)
|
assert(state.connection == Connection.AvatarSelection)
|
||||||
send(CharacterRequestMessage(charId, CharacterRequestAction.Select)).require
|
send(CharacterRequestMessage(charId, CharacterRequestAction.Select)).require
|
||||||
waitFor[LoadMapMessage](timeout = 15.seconds).require
|
waitFor[LoadMapMessage](timeout = 30.seconds).require
|
||||||
}
|
}
|
||||||
|
|
||||||
def createCharacter(): Unit = {
|
def createCharacter(name: String): Unit = {
|
||||||
???
|
assert(state.connection == Connection.AvatarSelection)
|
||||||
|
send(CharacterCreateRequestMessage(name, 0, CharacterVoice.Voice1, CharacterSex.Male, PlanetSideEmpire.TR)).require
|
||||||
|
val r = waitFor[ActionResultMessage](timeout = 15.seconds).require
|
||||||
|
assert(r.errorCode == None)
|
||||||
|
while (true) {
|
||||||
|
val r = waitFor[CharacterInfoMessage]().require
|
||||||
|
if (r.finished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteCharacter(charId: Long): Unit = {
|
def deleteCharacter(charId: Long): Unit = {
|
||||||
??? // never been tested
|
|
||||||
assert(state.connection == Connection.AvatarSelection)
|
assert(state.connection == Connection.AvatarSelection)
|
||||||
send(CharacterRequestMessage(charId, CharacterRequestAction.Delete)).require
|
send(CharacterRequestMessage(charId, CharacterRequestAction.Delete)).require
|
||||||
}
|
}
|
||||||
|
|
@ -293,13 +328,11 @@ class Client(username: String, password: String) {
|
||||||
private def _process(packet: PlanetSidePacket): Unit = {
|
private def _process(packet: PlanetSidePacket): Unit = {
|
||||||
packet match {
|
packet match {
|
||||||
case _: KeepAliveMessage => ()
|
case _: KeepAliveMessage => ()
|
||||||
case _: LoadMapMessage =>
|
case _: LoadMapMessage =>
|
||||||
log.info(s"process: ${packet}")
|
|
||||||
send(BeginZoningMessage()).require
|
send(BeginZoningMessage()).require
|
||||||
_state = state.update(packet)
|
_state = state.update(packet)
|
||||||
case packet: PlanetSideGamePacket =>
|
case packet: PlanetSideGamePacket =>
|
||||||
_state = state.update(packet)
|
_state = state.update(packet)
|
||||||
log.info(s"process: ${packet}")
|
|
||||||
()
|
()
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
|
|
@ -346,10 +379,6 @@ class Client(username: String, password: String) {
|
||||||
sequence: Option[Int],
|
sequence: Option[Int],
|
||||||
crypto: Option[CryptoCoding]
|
crypto: Option[CryptoCoding]
|
||||||
): Attempt[BitVector] = {
|
): Attempt[BitVector] = {
|
||||||
packet match {
|
|
||||||
case _: KeepAliveMessage => ()
|
|
||||||
case _ => log.info(s"send: ${packet}")
|
|
||||||
}
|
|
||||||
PacketCoding.marshalPacket(packet, sequence, crypto) match {
|
PacketCoding.marshalPacket(packet, sequence, crypto) match {
|
||||||
case Successful(payload) =>
|
case Successful(payload) =>
|
||||||
send(payload.toByteArray)
|
send(payload.toByteArray)
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,14 @@ case class State(
|
||||||
case LoginRespMessage(token, _, _, _, _, _, _) => this.copy(token = Some(token))
|
case LoginRespMessage(token, _, _, _, _, _, _) => this.copy(token = Some(token))
|
||||||
case VNLWorldStatusMessage(_, worlds) => this.copy(worlds = worlds, connection = Connection.WorldSelection)
|
case VNLWorldStatusMessage(_, worlds) => this.copy(worlds = worlds, connection = Connection.WorldSelection)
|
||||||
case ObjectCreateDetailedMessage(_, objectClass, guid, _, _) => this.copy(objects = objects ++ Seq(guid.guid))
|
case ObjectCreateDetailedMessage(_, objectClass, guid, _, _) => this.copy(objects = objects ++ Seq(guid.guid))
|
||||||
case message @ CharacterInfoMessage(_, _, _, _, _, _) =>
|
case message @ CharacterInfoMessage(_, _, _, _, finished, _) =>
|
||||||
this.copy(characters = characters ++ Seq(message), connection = Connection.AvatarSelection)
|
// if finished is true, it is not real character but rather signal that list is complete
|
||||||
|
if (finished) {
|
||||||
|
this.copy(connection = Connection.AvatarSelection)
|
||||||
|
} else {
|
||||||
|
this.copy(characters = characters ++ Seq(message), connection = Connection.AvatarSelection)
|
||||||
|
}
|
||||||
|
|
||||||
case _ => this
|
case _ => this
|
||||||
}).copy(avatar = avatar.update(packet))
|
}).copy(avatar = avatar.update(packet))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue