Merge branch 'master' into reduce-psm-load

This commit is contained in:
Fate-JH 2023-04-21 23:31:42 -04:00 committed by GitHub
commit 61a75f13cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 468298 additions and 468246 deletions

1
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1 @@
FROM mcr.microsoft.com/vscode/devcontainers/base:debian

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

View 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
View file

@ -0,0 +1,8 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View file

@ -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

View file

@ -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

View file

@ -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("."))

View file

@ -1 +1 @@
sbt.version = 1.4.5 sbt.version = 1.8.2

View file

@ -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.4.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.31") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4")

View file

@ -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()
@ -111,7 +108,8 @@ 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
@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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;
@ -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
} }
} }

View file

@ -298,7 +298,7 @@ object AvatarActor {
} }
val fireMode = tool.AmmoSlots(ammoSlots) val fireMode = tool.AmmoSlots(ammoSlots)
fireMode.AmmoTypeIndex = ammoTypeIndex fireMode.AmmoTypeIndex = ammoTypeIndex
fireMode.Box = AmmoBox(AmmoBoxDefinition(ammoBoxDefinition)) fireMode.Box = AmmoBox(DefinitionUtil.idToDefinition(ammoBoxDefinition).asInstanceOf[AmmoBoxDefinition])
ammoCount.collect { ammoCount.collect {
case count if restoreAmmo => fireMode.Magazine = count case count if restoreAmmo => fireMode.Magazine = count
} }
@ -461,7 +461,6 @@ object AvatarActor {
} }
} }
def displayLookingForSquad(session: Session, state: Int): Unit = { def displayLookingForSquad(session: Session, state: Int): Unit = {
val player = session.player val player = session.player
session.zone.AvatarEvents ! AvatarServiceMessage( session.zone.AvatarEvents ! AvatarServiceMessage(
@ -477,7 +476,10 @@ object AvatarActor {
* @param func functionality that is called upon discovery of the character * @param func functionality that is called upon discovery of the character
* @return if found, the discovered avatar, the avatar's account id, and the avatar's faction affiliation * @return if found, the discovered avatar, the avatar's account id, and the avatar's faction affiliation
*/ */
def getLiveAvatarForFunc(name: String, func: (Long,String,Int)=>Unit): Option[(Avatar, Long, PlanetSideEmpire.Value)] = { def getLiveAvatarForFunc(
name: String,
func: (Long, String, Int) => Unit
): Option[(Avatar, Long, PlanetSideEmpire.Value)] = {
if (name.nonEmpty) { if (name.nonEmpty) {
LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(name) }).headOption match { LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(name) }).headOption match {
case Some(otherAvatar) => case Some(otherAvatar) =>
@ -500,7 +502,10 @@ object AvatarActor {
* otherwise, always returns `None` as if no avatar was discovered * otherwise, always returns `None` as if no avatar was discovered
* (the query is probably still in progress) * (the query is probably still in progress)
*/ */
def getAvatarForFunc(name: String, func: (Long,String,Int)=>Unit): Option[(Avatar, Long, PlanetSideEmpire.Value)] = { def getAvatarForFunc(
name: String,
func: (Long, String, Int) => Unit
): Option[(Avatar, Long, PlanetSideEmpire.Value)] = {
getLiveAvatarForFunc(name, func).orElse { getLiveAvatarForFunc(name, func).orElse {
if (name.nonEmpty) { if (name.nonEmpty) {
import ctx._ import ctx._
@ -540,7 +545,9 @@ object AvatarActor {
*/ */
def onlineIfNotIgnored(onlinePlayerName: String, observerName: String): Boolean = { def onlineIfNotIgnored(onlinePlayerName: String, observerName: String): Boolean = {
val onlinePlayerNameLower = onlinePlayerName.toLowerCase() val onlinePlayerNameLower = onlinePlayerName.toLowerCase()
LivePlayerList.WorldPopulation({ case (_, a) => a.name.toLowerCase().equals(onlinePlayerNameLower) }).headOption match { LivePlayerList
.WorldPopulation({ case (_, a) => a.name.toLowerCase().equals(onlinePlayerNameLower) })
.headOption match {
case Some(onlinePlayer) => onlineIfNotIgnored(onlinePlayer, observerName) case Some(onlinePlayer) => onlineIfNotIgnored(onlinePlayer, observerName)
case _ => false case _ => false
} }
@ -603,7 +610,8 @@ object AvatarActor {
case Success(data) if data.nonEmpty => case Success(data) if data.nonEmpty =>
out.completeWith(Future(data.head)) out.completeWith(Future(data.head))
case _ => case _ =>
ctx.run(query[persistence.Savedplayer] ctx.run(
query[persistence.Savedplayer]
.insert( .insert(
_.avatarId -> lift(avatarId), _.avatarId -> lift(avatarId),
_.px -> lift(0), _.px -> lift(0),
@ -689,7 +697,8 @@ object AvatarActor {
val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) }) val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
queryResult.onComplete { queryResult.onComplete {
case Success(results) if results.nonEmpty => case Success(results) if results.nonEmpty =>
ctx.run(query[persistence.Savedplayer] ctx.run(
query[persistence.Savedplayer]
.filter { _.avatarId == lift(avatarId) } .filter { _.avatarId == lift(avatarId) }
.update( .update(
_.px -> lift((position.x * 1000).toInt), _.px -> lift((position.x * 1000).toInt),
@ -727,7 +736,8 @@ object AvatarActor {
val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) }) val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
queryResult.onComplete { queryResult.onComplete {
case Success(results) if results.nonEmpty => case Success(results) if results.nonEmpty =>
ctx.run(query[persistence.Savedplayer] ctx.run(
query[persistence.Savedplayer]
.filter { _.avatarId == lift(avatarId) } .filter { _.avatarId == lift(avatarId) }
.update( .update(
_.px -> lift((position.x * 1000).toInt), _.px -> lift((position.x * 1000).toInt),
@ -763,7 +773,8 @@ object AvatarActor {
out.completeWith(Future(data.head)) out.completeWith(Future(data.head))
case _ => case _ =>
val now = LocalDateTime.now() val now = LocalDateTime.now()
ctx.run(query[persistence.Savedavatar] ctx.run(
query[persistence.Savedavatar]
.insert( .insert(
_.avatarId -> lift(avatarId), _.avatarId -> lift(avatarId),
_.forgetCooldown -> lift(now), _.forgetCooldown -> lift(now),
@ -792,7 +803,8 @@ object AvatarActor {
val queryResult = ctx.run(query[persistence.Savedavatar].filter { _.avatarId == lift(avatarId) }) val queryResult = ctx.run(query[persistence.Savedavatar].filter { _.avatarId == lift(avatarId) })
queryResult.onComplete { queryResult.onComplete {
case Success(results) if results.nonEmpty => case Success(results) if results.nonEmpty =>
ctx.run(query[persistence.Savedavatar] ctx.run(
query[persistence.Savedavatar]
.filter { _.avatarId == lift(avatarId) } .filter { _.avatarId == lift(avatarId) }
.update( .update(
_.purchaseCooldowns -> lift(buildClobfromCooldowns(avatar.cooldowns.purchase)), _.purchaseCooldowns -> lift(buildClobfromCooldowns(avatar.cooldowns.purchase)),
@ -959,7 +971,8 @@ class AvatarActor(
deleted.headOption match { deleted.headOption match {
case Some(a) if !a.deleted => case Some(a) if !a.deleted =>
val flagDeletion = for { val flagDeletion = for {
_ <- ctx.run(query[persistence.Avatar] _ <- ctx.run(
query[persistence.Avatar]
.filter(_.id == lift(id)) .filter(_.id == lift(id))
.update( .update(
_.deleted -> lift(true), _.deleted -> lift(true),
@ -1008,7 +1021,8 @@ class AvatarActor(
case LoginAvatar(replyTo) => case LoginAvatar(replyTo) =>
import ctx._ import ctx._
val avatarId = avatar.id val avatarId = avatar.id
ctx.run( ctx
.run(
query[persistence.Avatar] query[persistence.Avatar]
.filter(_.id == lift(avatarId)) .filter(_.id == lift(avatarId))
.map { c => (c.created, c.lastLogin) } .map { c => (c.created, c.lastLogin) }
@ -1031,12 +1045,12 @@ class AvatarActor(
persistence.Certification(Certification.ATV.value, avatarId), persistence.Certification(Certification.ATV.value, avatarId),
persistence.Certification(Certification.Harasser.value, avatarId) persistence.Certification(Certification.Harasser.value, avatarId)
) )
).foreach(c => query[persistence.Certification].insert(c)) ).foreach(c => query[persistence.Certification].insertValue(c))
) )
_ <- ctx.run( _ <- ctx.run(
liftQuery( liftQuery(
List(persistence.Shortcut(avatarId, 0, 0, "medkit")) List(persistence.Shortcut(avatarId, 0, 0, "medkit"))
).foreach(c => query[persistence.Shortcut].insert(c)) ).foreach(c => query[persistence.Shortcut].insertValue(c))
) )
} yield true } yield true
inits.onComplete { inits.onComplete {
@ -1109,7 +1123,8 @@ class AvatarActor(
val replace = certification.replaces.intersect(avatar.certifications) val replace = certification.replaces.intersect(avatar.certifications)
Future Future
.sequence(replace.map(cert => { .sequence(replace.map(cert => {
ctx.run( ctx
.run(
query[persistence.Certification] query[persistence.Certification]
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
.filter(_.id == lift(cert.value)) .filter(_.id == lift(cert.value))
@ -1129,7 +1144,8 @@ class AvatarActor(
PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
) )
} }
ctx.run( ctx
.run(
query[persistence.Certification] query[persistence.Certification]
.insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id)) .insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id))
) )
@ -1180,7 +1196,8 @@ class AvatarActor(
avatar.certifications avatar.certifications
.intersect(requiredByCert) .intersect(requiredByCert)
.map(cert => { .map(cert => {
ctx.run( ctx
.run(
query[persistence.Certification] query[persistence.Certification]
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
.filter(_.id == lift(cert.value)) .filter(_.id == lift(cert.value))
@ -1329,7 +1346,8 @@ class AvatarActor(
index match { index match {
case Some(_index) => case Some(_index) =>
import ctx._ import ctx._
ctx.run( ctx
.run(
query[persistence.Implant] query[persistence.Implant]
.filter(_.name == lift(definition.Name)) .filter(_.name == lift(definition.Name))
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
@ -1462,7 +1480,9 @@ class AvatarActor(
case UpdatePurchaseTime(definition, time) => case UpdatePurchaseTime(definition, time) =>
var newTimes = avatar.cooldowns.purchase var newTimes = avatar.cooldowns.purchase
AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach { AvatarActor
.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition))
.foreach {
case (item, name) => case (item, name) =>
Avatar.purchaseCooldowns.get(item) match { Avatar.purchaseCooldowns.get(item) match {
case Some(cooldown) => case Some(cooldown) =>
@ -1682,7 +1702,9 @@ class AvatarActor(
case -1 => previousRibbonBars case -1 => previousRibbonBars
case n => AvatarActor.changeRibbons(previousRibbonBars, MeritCommendation.None, RibbonBarSlot(n)) case n => AvatarActor.changeRibbons(previousRibbonBars, MeritCommendation.None, RibbonBarSlot(n))
} }
replaceAvatar(avatar.copy(decoration = decor.copy(ribbonBars = AvatarActor.changeRibbons(useRibbonBars, ribbon, bar)))) replaceAvatar(
avatar.copy(decoration = decor.copy(ribbonBars = AvatarActor.changeRibbons(useRibbonBars, ribbon, bar)))
)
val player = session.get.player val player = session.get.player
val zone = player.Zone val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
@ -1706,9 +1728,11 @@ class AvatarActor(
case _ => false case _ => false
}) })
if (isDifferentShortcut) { if (isDifferentShortcut) {
if (!isMacroShortcut && avatar.shortcuts.flatten.exists { if (
a => AvatarShortcut.equals(shortcut, a) !isMacroShortcut && avatar.shortcuts.flatten.exists { a =>
}) { AvatarShortcut.equals(shortcut, a)
}
) {
//duplicate implant or medkit found //duplicate implant or medkit found
if (shortcut.isInstanceOf[Shortcut.Implant]) { if (shortcut.isInstanceOf[Shortcut.Implant]) {
//duplicate implant //duplicate implant
@ -1716,11 +1740,17 @@ class AvatarActor(
case Some(existingShortcut: AvatarShortcut) => case Some(existingShortcut: AvatarShortcut) =>
//redraw redundant shortcut slot with existing shortcut //redraw redundant shortcut slot with existing shortcut
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(session.get.player.GUID, slot + 1, Some(AvatarShortcut.convert(existingShortcut))) CreateShortcutMessage(
session.get.player.GUID,
slot + 1,
Some(AvatarShortcut.convert(existingShortcut))
)
) )
case _ => case _ =>
//blank shortcut slot //blank shortcut slot
sessionActor ! SessionActor.SendResponse(CreateShortcutMessage(session.get.player.GUID, slot + 1, None)) sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(session.get.player.GUID, slot + 1, None)
)
} }
} }
} else { } else {
@ -1771,7 +1801,8 @@ class AvatarActor(
avatar.shortcuts.lift(slot).flatten match { avatar.shortcuts.lift(slot).flatten match {
case None => ; case None => ;
case Some(_) => case Some(_) =>
ctx.run(query[persistence.Shortcut] ctx.run(
query[persistence.Shortcut]
.filter(_.avatarId == lift(avatar.id.toLong)) .filter(_.avatarId == lift(avatar.id.toLong))
.filter(_.slot == lift(slot)) .filter(_.slot == lift(slot))
.delete .delete
@ -1803,11 +1834,15 @@ class AvatarActor(
val result = for { val result = for {
//log this login //log this login
_ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)) _ <- ctx.run(
query[persistence.Avatar]
.filter(_.id == lift(avatarId))
.update(_.lastLogin -> lift(LocalDateTime.now())) .update(_.lastLogin -> lift(LocalDateTime.now()))
) )
//log this choice of faction (no empire switching) //log this choice of faction (no empire switching)
_ <- ctx.run(query[persistence.Account].filter(_.id == lift(accountId)) _ <- ctx.run(
query[persistence.Account]
.filter(_.id == lift(accountId))
.update(_.lastFactionId -> lift(avatar.faction.id)) .update(_.lastFactionId -> lift(avatar.faction.id))
) )
//retrieve avatar data //retrieve avatar data
@ -1887,7 +1922,8 @@ class AvatarActor(
val p = Promise[Unit]() val p = Promise[Unit]()
import ctx._ import ctx._
ctx.run( ctx
.run(
query[persistence.Avatar] query[persistence.Avatar]
.filter(_.id == lift(avatar.id)) .filter(_.id == lift(avatar.id))
.update(_.cosmetics -> lift(Some(Cosmetic.valuesToObjectCreateValue(cosmetics)): Option[Int])) .update(_.cosmetics -> lift(Some(Cosmetic.valuesToObjectCreateValue(cosmetics)): Option[Int]))
@ -2177,6 +2213,7 @@ class AvatarActor(
secondsSinceLastLogin secondsSinceLastLogin
) )
) )
/** After the user has selected a character to load from the "character select screen," /** After the user has selected a character to load from the "character select screen,"
* the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected. * the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected.
* Characters that were not selected may be destroyed along with their temporary GUIDs. * Characters that were not selected may be destroyed along with their temporary GUIDs.
@ -2471,16 +2508,18 @@ class AvatarActor(
val locker = Avatar.makeLocker() val locker = Avatar.makeLocker()
saveLockerFunc = storeLocker saveLockerFunc = storeLocker
val out = Promise[LockerContainer]() val out = Promise[LockerContainer]()
ctx.run(query[persistence.Locker].filter(_.avatarId == lift(charId))) ctx
.run(query[persistence.Locker].filter(_.avatarId == lift(charId)))
.onComplete { .onComplete {
case Success(entry) if entry.nonEmpty => case Success(entry) if entry.nonEmpty =>
AvatarActor.buildContainedEquipmentFromClob(locker, entry.head.items, log, restoreAmmo = true) AvatarActor.buildContainedEquipmentFromClob(locker, entry.head.items, log, restoreAmmo = true)
out.completeWith(Future(locker)) out.completeWith(Future(locker))
case Success(_) => case Success(_) =>
//no locker, or maybe default empty locker? //no locker, or maybe default empty locker?
ctx.run(query[persistence.Locker].insert(_.avatarId -> lift(avatar.id), _.items -> lift(""))) ctx
.onComplete { .run(query[persistence.Locker].insert(_.avatarId -> lift(avatar.id), _.items -> lift("")))
_ => out.completeWith(Future(locker)) .onComplete { _ =>
out.completeWith(Future(locker))
} }
case Failure(e) => case Failure(e) =>
saveLockerFunc = doNotStoreLocker saveLockerFunc = doNotStoreLocker
@ -2490,23 +2529,24 @@ class AvatarActor(
out.future out.future
} }
def loadFriendList(avatarId: Long): Future[List[AvatarFriend]] = { def loadFriendList(avatarId: Long): Future[List[AvatarFriend]] = {
import ctx._ import ctx._
val out: Promise[List[AvatarFriend]] = Promise() val out: Promise[List[AvatarFriend]] = Promise()
val queryResult = ctx.run( val queryResult = ctx.run(
query[persistence.Friend].filter { _.avatarId == lift(avatarId) } query[persistence.Friend]
.filter { _.avatarId == lift(avatarId) }
.join(query[persistence.Avatar]) .join(query[persistence.Avatar])
.on { case (friend, avatar) => friend.charId == avatar.id } .on { case (friend, avatar) => friend.charId == avatar.id }
.map { case (_, avatar) => (avatar.id, avatar.name, avatar.factionId) } .map { case (_, avatar) => (avatar.id, avatar.name, avatar.factionId) }
) )
queryResult.onComplete { queryResult.onComplete {
case Success(list) => case Success(list) =>
out.completeWith(Future( out.completeWith(
Future(
list.map { case (id, name, faction) => AvatarFriend(id, name, PlanetSideEmpire(faction)) }.toList list.map { case (id, name, faction) => AvatarFriend(id, name, PlanetSideEmpire(faction)) }.toList
)) )
)
case _ => case _ =>
out.completeWith(Future(List.empty[AvatarFriend])) out.completeWith(Future(List.empty[AvatarFriend]))
} }
@ -2518,16 +2558,19 @@ class AvatarActor(
val out: Promise[List[AvatarIgnored]] = Promise() val out: Promise[List[AvatarIgnored]] = Promise()
val queryResult = ctx.run( val queryResult = ctx.run(
query[persistence.Ignored].filter { _.avatarId == lift(avatarId) } query[persistence.Ignored]
.filter { _.avatarId == lift(avatarId) }
.join(query[persistence.Avatar]) .join(query[persistence.Avatar])
.on { case (friend, avatar) => friend.charId == avatar.id } .on { case (friend, avatar) => friend.charId == avatar.id }
.map { case (_, avatar) => (avatar.id, avatar.name) } .map { case (_, avatar) => (avatar.id, avatar.name) }
) )
queryResult.onComplete { queryResult.onComplete {
case Success(list) => case Success(list) =>
out.completeWith(Future( out.completeWith(
Future(
list.map { case (id, name) => AvatarIgnored(id, name) }.toList list.map { case (id, name) => AvatarIgnored(id, name) }.toList
)) )
)
case _ => case _ =>
out.completeWith(Future(List.empty[AvatarIgnored])) out.completeWith(Future(List.empty[AvatarIgnored]))
} }
@ -2539,13 +2582,15 @@ class AvatarActor(
val out: Promise[Array[Option[AvatarShortcut]]] = Promise() val out: Promise[Array[Option[AvatarShortcut]]] = Promise()
val queryResult = ctx.run( val queryResult = ctx.run(
query[persistence.Shortcut].filter { _.avatarId == lift(avatarId) } query[persistence.Shortcut]
.filter { _.avatarId == lift(avatarId) }
.map { shortcut => (shortcut.slot, shortcut.purpose, shortcut.tile, shortcut.effect1, shortcut.effect2) } .map { shortcut => (shortcut.slot, shortcut.purpose, shortcut.tile, shortcut.effect1, shortcut.effect2) }
) )
val output = Array.fill[Option[AvatarShortcut]](64)(None) val output = Array.fill[Option[AvatarShortcut]](64)(None)
queryResult.onComplete { queryResult.onComplete {
case Success(list) => case Success(list) =>
list.foreach { case (slot, purpose, tile, effect1, effect2) => list.foreach {
case (slot, purpose, tile, effect1, effect2) =>
output.update(slot, Some(AvatarShortcut(purpose, tile, effect1.getOrElse(""), effect2.getOrElse("")))) output.update(slot, Some(AvatarShortcut(purpose, tile, effect1.getOrElse(""), effect2.getOrElse(""))))
} }
out.completeWith(Future(output)) out.completeWith(Future(output))
@ -2660,6 +2705,7 @@ class AvatarActor(
def transformFriendsList(): List[GameFriend] = { def transformFriendsList(): List[GameFriend] = {
avatar.people.friend.map { f => GameFriend(f.name, f.online) } avatar.people.friend.map { f => GameFriend(f.name, f.online) }
} }
/** /**
* Transform the ignored players list in a list of packet entities. * Transform the ignored players list in a list of packet entities.
* @return a list of `Friends` suitable for putting into a packet * @return a list of `Friends` suitable for putting into a packet
@ -2667,6 +2713,7 @@ class AvatarActor(
def transformIgnoredList(): List[GameFriend] = { def transformIgnoredList(): List[GameFriend] = {
avatar.people.ignored.map { f => GameFriend(f.name, f.online) } avatar.people.ignored.map { f => GameFriend(f.name, f.online) }
} }
/** /**
* Reload the list of friend players or ignored players for the client. * Reload the list of friend players or ignored players for the client.
* This does not update any player's online status, but merely reloads current states. * This does not update any player's online status, but merely reloads current states.
@ -2693,16 +2740,20 @@ class AvatarActor(
case Some(_) => ; case Some(_) => ;
case None => case None =>
import ctx._ import ctx._
ctx.run(query[persistence.Friend] ctx.run(
query[persistence.Friend]
.insert( .insert(
_.avatarId -> lift(avatar.id.toLong), _.avatarId -> lift(avatar.id.toLong),
_.charId -> lift(charId) _.charId -> lift(charId)
) )
) )
val isOnline = onlineIfNotIgnoredEitherWay(avatar, name) val isOnline = onlineIfNotIgnoredEitherWay(avatar, name)
replaceAvatar(avatar.copy( replaceAvatar(
people = people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline)) avatar.copy(
)) people =
people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline))
)
)
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline))) sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline)))
sessionActor ! SessionActor.CharSaved sessionActor ! SessionActor.CharSaved
} }
@ -2724,7 +2775,8 @@ class AvatarActor(
) )
case None => ; case None => ;
} }
ctx.run(query[persistence.Friend] ctx.run(
query[persistence.Friend]
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
.filter(_.charId == lift(charId)) .filter(_.charId == lift(charId))
.delete .delete
@ -2734,7 +2786,6 @@ class AvatarActor(
} }
/** /**
*
* @param name unique character name * @param name unique character name
* @return if the avatar is found, that avatar's unique identifier and the avatar's faction affiliation * @return if the avatar is found, that avatar's unique identifier and the avatar's faction affiliation
*/ */
@ -2752,11 +2803,13 @@ class AvatarActor(
case None => case None =>
(None, false) (None, false)
} }
replaceAvatar(avatar.copy( replaceAvatar(
avatar.copy(
people = people.copy( people = people.copy(
friend = people.friend.filterNot { _.name.equals(name) } :+ otherFriend.copy(online = online) friend = people.friend.filterNot { _.name.equals(name) } :+ otherFriend.copy(online = online)
) )
)) )
)
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.UpdateFriend, GameFriend(name, online))) sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.UpdateFriend, GameFriend(name, online)))
out out
case None => case None =>
@ -2782,7 +2835,8 @@ class AvatarActor(
case Some(_) => ; case Some(_) => ;
case None => case None =>
import ctx._ import ctx._
ctx.run(query[persistence.Ignored] ctx.run(
query[persistence.Ignored]
.insert( .insert(
_.avatarId -> lift(avatar.id.toLong), _.avatarId -> lift(avatar.id.toLong),
_.charId -> lift(charId) _.charId -> lift(charId)
@ -2791,7 +2845,9 @@ class AvatarActor(
replaceAvatar( replaceAvatar(
avatar.copy(people = people.copy(ignored = people.ignored :+ AvatarIgnored(charId, name))) avatar.copy(people = people.copy(ignored = people.ignored :+ AvatarIgnored(charId, name)))
) )
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name))) sessionActor ! SessionActor.UpdateIgnoredPlayers(
FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name))
)
sessionActor ! SessionActor.CharSaved sessionActor ! SessionActor.CharSaved
} }
} }
@ -2814,12 +2870,15 @@ class AvatarActor(
) )
case None => ; case None => ;
} }
ctx.run(query[persistence.Ignored] ctx.run(
query[persistence.Ignored]
.filter(_.avatarId == lift(avatar.id.toLong)) .filter(_.avatarId == lift(avatar.id.toLong))
.filter(_.charId == lift(charId)) .filter(_.charId == lift(charId))
.delete .delete
) )
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name))) sessionActor ! SessionActor.UpdateIgnoredPlayers(
FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name))
)
sessionActor ! SessionActor.CharSaved sessionActor ! SessionActor.CharSaved
} }
@ -2854,7 +2913,8 @@ class AvatarActor(
val implants = avatar.implants.zipWithIndex.map { val implants = avatar.implants.zipWithIndex.map {
case (implant, index) => case (implant, index) =>
if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) { if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) {
ctx.run( ctx
.run(
query[persistence.Implant] query[persistence.Implant]
.filter(_.name == lift(implant.get.definition.Name)) .filter(_.name == lift(implant.get.definition.Name))
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
@ -2862,7 +2922,9 @@ class AvatarActor(
) )
.onComplete { .onComplete {
case Success(_) => case Success(_) =>
sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)) sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)
)
case Failure(exception) => case Failure(exception) =>
log.error(exception)("db failure") log.error(exception)("db failure")
} }

View file

@ -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,12 +113,9 @@ 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")
ServiceManager.serviceManager ! Lookup("galaxy") ServiceManager.serviceManager ! Lookup("galaxy")
@ -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 = {
@ -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")
} }
} }

View file

@ -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,9 +1245,18 @@ class SessionData(
} }
} }
private def handleUseDoor(door: Door): Unit = { private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
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) door.Actor ! CommonMessages.Use(player)
} }
}
private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")

View file

@ -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 _ => ()
} }
} }
} }

View file

@ -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
//see above in VehicleResponse.TransferPassenger case
sessionData.zoning.interstellarFerry = None 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))

View file

@ -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
} }
} }
} }

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

View file

@ -256,5 +256,3 @@ object Tool {
def Definition: FireModeDefinition = fdef def Definition: FireModeDefinition = fdef
} }
} }

View file

@ -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

View file

@ -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)
)
}
}
} }

View file

@ -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
} }

View file

@ -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,23 +64,25 @@ trait CargoBehavior {
} }
def startCargoDismounting(bailed: Boolean): Unit = { def startCargoDismounting(bailed: Boolean): Unit = {
if (!startCargoDismountingNoCleanup(bailed)) {
isDismounting = None
CargoObject.MountedIn = None
}
}
def startCargoDismountingNoCleanup(bailed: Boolean): Boolean = {
val obj = CargoObject val obj = CargoObject
obj.Zone.GUID(obj.MountedIn) match { obj.Zone.GUID(obj.MountedIn)
case Some(carrier: Vehicle) => .collect { case carrier: Vehicle =>
carrier.CargoHolds.find { case (_, hold) => hold.occupant.contains(obj) } match { (carrier, carrier.CargoHolds.find { case (_, hold) => hold.occupant.contains(obj) })
case Some((mountPoint, _)) }
.collect { case (carrier, Some((mountPoint, _)))
if isDismounting.isEmpty && isMounting.isEmpty => if isDismounting.isEmpty && isMounting.isEmpty =>
isDismounting = obj.MountedIn isDismounting = obj.MountedIn
carrier.Actor ! CarrierBehavior.CheckCargoDismount(obj.GUID, mountPoint, 0, bailed) carrier.Actor ! CarrierBehavior.CheckCargoDismount(obj.GUID, mountPoint, 0, bailed)
true
case _ =>
obj.MountedIn = None
isDismounting = None
}
case _ =>
obj.MountedIn = None
isDismounting = None
} }
.nonEmpty
} }
} }

View file

@ -654,4 +654,3 @@ object CarrierBehavior {
msgs msgs
} }
} }

View file

@ -40,4 +40,3 @@ trait MountableWeapons
def Definition: MountableWeaponsDefinition def Definition: MountableWeaponsDefinition
} }

View file

@ -55,4 +55,3 @@ class AmsControl(vehicle: Vehicle)
} }
} }
} }

View file

@ -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)
} }
} }

View file

@ -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) {

View file

@ -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()}'"))
} }
} }

View file

@ -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)

View file

@ -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]
} }

View file

@ -59,12 +59,15 @@ object GenericActionMessage extends Marshallable[GenericActionMessage] {
}) })
} }
private val genericActionCodec = uint(bits = 6).xmap[GenericAction]({ private val genericActionCodec = uint(bits = 6).xmap[GenericAction](
i => GenericAction.values.find { _.value == i } match { { i =>
GenericAction.values.find { _.value == i } match {
case Some(enum) => enum case Some(enum) => enum
case None => GenericAction.Unknown(i) 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]
} }

View file

@ -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))
} }
/** /**
@ -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](

View file

@ -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)

View file

@ -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(

View file

@ -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)
} }
/** /**

View file

@ -332,4 +332,3 @@ object MountableInventory {
} }
} }
} }

View file

@ -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 {

View file

@ -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.

View file

@ -25,14 +25,16 @@ 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
extends CharacterSex(
value = 1, value = 1,
pronounSubject = "he", pronounSubject = "he",
pronounObject = "him", pronounObject = "him",
possessive = "his" possessive = "his"
) )
case object Female extends CharacterSex( case object Female
extends CharacterSex(
value = 2, value = 2,
pronounSubject = "she", pronounSubject = "she",
pronounObject = "her", pronounObject = "her",
@ -41,5 +43,5 @@ object CharacterSex extends IntEnum[CharacterSex] {
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)
} }

View file

@ -15,5 +15,5 @@ object ExperienceType extends IntEnum[ExperienceType] {
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))
} }

View file

@ -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)
} }
) )
} }

View file

@ -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))
} }

View file

@ -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

View file

@ -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,19 +62,18 @@ 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)
@ -82,7 +81,6 @@ object Config {
sys.exit(1) sys.exit(1)
} }
} }
}
case class AppConfig( case class AppConfig(
bind: String, bind: String,
@ -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(

View file

@ -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

View file

@ -45,4 +45,3 @@ class CaptureFlagUpdateMessageTest extends Specification with Debug {
pkt mustEqual stringOne pkt mustEqual stringOne
} }
} }

View file

@ -65,4 +65,3 @@ class ComponentDamageMessageTest extends Specification {
pkt mustEqual string_off pkt mustEqual string_off
} }
} }

View file

@ -54,4 +54,3 @@ class FrameVehicleStateMessageTest extends Specification {
pkt mustEqual string pkt mustEqual string
} }
} }

View file

@ -28,4 +28,3 @@ class GenericObjectActionAtPositionMessageTest extends Specification {
pkt mustEqual string pkt mustEqual string
} }
} }

View file

@ -347,4 +347,3 @@ class BattleframeRoboticsTest extends Specification {
} }
} }
} }

View file

@ -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)
} }
} }

View file

@ -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)}")

View file

@ -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")
for (i <- 0 until 20) {
val id = i
new Thread {
override def run: Unit = {
val client = new Client(s"bot${id}", "bot")
client.login(new InetSocketAddress("localhost", 51000)) client.login(new InetSocketAddress("localhost", 51000))
client.joinWorld(client.state.worlds.head) client.joinWorld(client.state.worlds.head)
if (client.state.characters.isEmpty) {
client.createCharacter(s"bot${id}")
}
client.selectCharacter(client.state.characters.head.charId) client.selectCharacter(client.state.characters.head.charId)
client.startTasks() client.startTasks()
client.send(ChatMsg(ChatMessageType.CMT_ZONE, wideContents = false, "", "z1", None))
while (true) { while (true) {
client.updateAvatar(client.state.avatar.copy(crouching = !client.state.avatar.crouching)) client.updateAvatar(client.state.avatar.copy(crouching = !client.state.avatar.crouching))
Thread.sleep(2000) Thread.sleep(2000)
//Thread.sleep(Int.MaxValue)
} }
} }
}.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
} }
@ -294,12 +329,10 @@ class Client(username: String, password: String) {
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)

View file

@ -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, _) =>
// 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) this.copy(characters = characters ++ Seq(message), connection = Connection.AvatarSelection)
}
case _ => this case _ => this
}).copy(avatar = avatar.update(packet)) }).copy(avatar = avatar.update(packet))