Merge branch 'master' into squad-again-again

This commit is contained in:
Fate-JH 2025-07-31 01:33:34 -04:00 committed by GitHub
commit fa26224316
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 8501 additions and 4254 deletions

View file

@ -23,7 +23,7 @@ add_property boomer_trigger equiptime 500
add_property chainblade equiptime 250 add_property chainblade equiptime 250
add_property chainblade holstertime 250 add_property chainblade holstertime 250
add_property colossus_flight requirement_award0 false add_property colossus_flight requirement_award0 false
add_property command_detonater allowed false add_property command_detonater allowed true
add_property command_detonater equiptime 500 add_property command_detonater equiptime 500
add_property command_detonater holstertime 500 add_property command_detonater holstertime 500
add_property cycler equiptime 600 add_property cycler equiptime 600
@ -96,3 +96,30 @@ add_property suppressor holstertime 600
add_property trek equiptime 500 add_property trek equiptime 500
add_property trek holstertime 500 add_property trek holstertime 500
add_property vulture requirement_award0 false add_property vulture requirement_award0 false
add_property aphelion allowed true
add_property aphelion_flight allowed false
add_property aphelion_gunner allowed true
add_property aphelion_armor_siphon allowed false
add_property aphelion_armor_siphon_left allowed false
add_property aphelion_armor_siphon_right allowed false
add_property aphelion_ntu_siphon allowed false
add_property aphelion_ntu_siphon_left allowed false
add_property aphelion_ntu_siphon_right allowed false
add_property colossus allowed true
add_property colossus_flight allowed false
add_property colossus_gunner allowed true
add_property colossus_armor_siphon allowed false
add_property colossus_armor_siphon_left allowed false
add_property colossus_armor_siphon_right allowed false
add_property colossus_ntu_siphon allowed false
add_property colossus_ntu_siphon_left allowed false
add_property colossus_ntu_siphon_right allowed false
add_property peregrine allowed true
add_property peregrine_flight allowed false
add_property peregrine_gunner allowed true
add_property peregrine_armor_siphon allowed false
add_property peregrine_armor_siphon_left allowed false
add_property peregrine_armor_siphon_right allowed false
add_property peregrine_ntu_siphon allowed false
add_property peregrine_ntu_siphon_left allowed false
add_property peregrine_ntu_siphon_right allowed false

View file

@ -23,11 +23,11 @@ world {
ports = [] ports = []
# The name of the server as displayed in the server browser. # The name of the server as displayed in the server browser.
server-name = PSForever server-name = "\\#1EACF9P\\#E87BE8S\\#F93F4E4ever"
# How the server is displayed in the server browser. # How the server is displayed in the server browser.
# One of: released beta development # One of: released beta development
server-type = released server-type = development
} }
# Admin API configuration # Admin API configuration
@ -88,6 +88,9 @@ game {
# Purchases timers for the battleframe robotics vehicles all update at the same time when either of them would update # Purchases timers for the battleframe robotics vehicles all update at the same time when either of them would update
shared-bfr-cooldown = yes shared-bfr-cooldown = yes
# How long the countdown timer is when a facility (main overworld base) is hacked
facility-hack-time = 15.minutes
# HART system, shuttles and facilities # HART system, shuttles and facilities
hart { hart {
# How long the shuttle is not boarding passengers (going through the motions) # How long the shuttle is not boarding passengers (going through the motions)
@ -408,8 +411,14 @@ game {
base = 25 base = 25
} }
{ {
name = "router" name = "router-driver"
base = 15 base = 15
shots-multiplier = 1.0
}
{
name = "telepad-use"
base = 20
shots-multiplier = 1.0
} }
{ {
name = "hotdrop" name = "hotdrop"
@ -460,7 +469,7 @@ game {
# If a player died while carrying an lattice logic unit, # If a player died while carrying an lattice logic unit,
# and satisfies the carrying duration, # and satisfies the carrying duration,
# award the player who is accredited with the kill command experience. # award the player who is accredited with the kill command experience.
llu-slayer-credit = 200 llu-slayer-credit = 0
# The maximum command experience that can be earned in a facility capture based on squad size # The maximum command experience that can be earned in a facility capture based on squad size
maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860] maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
# When the cep has to be capped for squad size, add a small value to the capped value # When the cep has to be capped for squad size, add a small value to the capped value
@ -499,6 +508,18 @@ game {
capture-experience-points-modifier = 1f capture-experience-points-modifier = 1f
# Don't forget to pay back that debt. # Don't forget to pay back that debt.
} }
alert {
# When a certain number of enemy players are within the SOI of a facility, an alert (DensityLevelUpdateMessage)
# will be dispatched to all players. Players of the owning faction will receive a chat warning (if in
# the same zone) and the map will flash the alert level over the facility until it changes
# Wiki says 25-30
yellow = 20
# Wiki says 30-60
orange = 30
# Wiki says 60+
red = 60
}
} }
anti-cheat { anti-cheat {

View file

@ -24,7 +24,6 @@ import org.joda.time.LocalDateTime
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.matching.Regex
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
object LoginActor { object LoginActor {
@ -33,94 +32,31 @@ object LoginActor {
private case object UpdateServerList extends Command private case object UpdateServerList extends Command
final case class ReceptionistListing(listing: Receptionist.Listing) extends Command final case class ReceptionistListing(listing: Receptionist.Listing) extends Command
/**
* What does a token do?
* No one knows.
* @return a 32-bit ascii string
*/
private def generateToken(): String = {
val r = new scala.util.Random
val sb = new mutable.StringBuilder
for (_ <- 1 to 31) {
sb.append(r.nextPrintableChar())
}
sb.toString
} }
class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) /**
extends Actor * Generates a new password from username and password combination,
with MDCContextAware { * hashing the initial password when salted by the username,
* mimicking the process the launcher follows.
import scala.concurrent.ExecutionContext.Implicits.global * @param username part of the original details
* @param password part of the original details
val usernameRegex: Regex = """[A-Za-z0-9]{3,}""".r * @param rounds number of times cryptographic mutation occurs
* @return new password
var leftRef: ActorRef = Default.Actor */
var rightRef: ActorRef = Default.Actor private def generateNewPassword(username: String, password: String, rounds: Int): String = {
var accountIntermediary: ActorRef = Default.Actor
var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor
var updateServerListTask: Cancellable = Default.Cancellable
var ipAddress: String = ""
var hostName: String = ""
var canonicalHostName: String = ""
var port: Int = 0
val serverName: String = Config.app.world.serverName
val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
private val bcryptRounds = 12
ServiceManager.serviceManager ! Lookup("accountIntermediary")
ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self)
override def postStop(): Unit = {
if (updateServerListTask != null)
updateServerListTask.cancel()
}
def receive: Receive = {
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
case SocketPane.SocketPaneKey.Listing(listings) =>
sockets = listings.head
case ReceiveIPAddress(address) =>
ipAddress = address.Address
hostName = address.HostName
canonicalHostName = address.CanonicalHostName
port = address.Port
case LoginActor.UpdateServerList =>
updateServerList()
case packet: PlanetSideGamePacket =>
handleGamePkt(packet)
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case default =>
failWithError(s"Invalid packet class received: $default")
}
def handleGamePkt(pkt: PlanetSideGamePacket): Unit =
pkt match {
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) =>
// TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
if (token.isDefined)
log.debug(s"New login UN:$username Token:${token.get}. $clientVersion")
else {
log.debug(s"New login UN:$username. $clientVersion")
}
requestAccountLogin(username, password, token)
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) =>
log.info(s"Request to connect to world '$name' ...")
sockets ! SocketPane.GetNextPort("world", context.self)
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
}
// generates a password from username and password combination
// mimics the process the launcher follows and hashes the password salted by the username
def generateNewPassword(username: String, password: String): String = {
// salt password hash with username (like the launcher does) (username + password) // salt password hash with username (like the launcher does) (username + password)
val saltedPassword = username.concat(password) val saltedPassword = username.concat(password)
// https://stackoverflow.com/a/46332228 // https://stackoverflow.com/a/46332228
@ -129,20 +65,178 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
.digest(saltedPassword.getBytes("UTF-8")) .digest(saltedPassword.getBytes("UTF-8"))
.map("%02x".format(_)).mkString .map("%02x".format(_)).mkString
// bcrypt hash for DB storage // bcrypt hash for DB storage
val bcryptedPassword = hashedPassword.bcryptBounded(bcryptRounds) val bcryptedPassword = hashedPassword.bcryptBounded(rounds)
bcryptedPassword bcryptedPassword
} }
def requestAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = { /**
tokenOpt match { * Remove flavor from the server name that should not show up in the log.
case Some(token) => accountLoginWithToken(token) * @param name original name
case None => accountLogin(username, passwordOpt.getOrElse("")) * @return sanitized name
*/
private def sanitizeServerName(name: String): String = {
//remove color codes from the server name - look for '\\#' followed by six characters or numbers
name.replaceAll("\\\\#[\\da-fA-F]{6}","")
} }
} }
def accountLogin(username: String, password: String): Unit = { class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
extends Actor
with MDCContextAware {
import scala.concurrent.ExecutionContext.Implicits.global
//private val usernameRegex: Regex = """[A-Za-z\d]{3,}""".r might be useful one day
private var accountIntermediary: ActorRef = Default.Actor
private var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor
private var updateServerListTask: Cancellable = Default.Cancellable
private var ipAddress: String = ""
private var hostName: String = ""
private var canonicalHostName: String = ""
private var port: Int = 0
private val serverName: String = Config.app.world.serverName
private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
private val bcryptRounds = 12
override def preStart(): Unit = {
super.preStart()
ServiceManager.serviceManager ! Lookup("accountIntermediary")
ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self)
}
override def postStop(): Unit = {
if (updateServerListTask != null)
updateServerListTask.cancel()
}
def receive: Receive = beforeLoginBehavior
private def persistentSetupMixinBehavior: Receive = {
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
accountIntermediary = endpoint
case SocketPane.SocketPaneKey.Listing(listings) =>
sockets = listings.head
}
private def idlingBehavior: Receive = persistentSetupMixinBehavior.orElse {
case _ => ()
}
private def beforeLoginBehavior: Receive = persistentSetupMixinBehavior.orElse {
case ReceiveIPAddress(address) =>
ipAddress = address.Address
hostName = address.HostName
canonicalHostName = address.CanonicalHostName
port = address.Port
context.become(idlingBehavior)
runLoginTest()
case _ => ()
}
private def accountLoginBehavior: Receive = persistentSetupMixinBehavior.orElse {
case packet: PlanetSideGamePacket =>
handleGamePktDuringLogin(packet)
case default =>
failWithError(s"Invalid packet class received: $default")
}
private def displayingServerListBehavior: Receive = persistentSetupMixinBehavior.orElse {
case packet: PlanetSideGamePacket =>
handleGamePktDuringWorldSelect(packet)
case LoginActor.UpdateServerList =>
updateServerList()
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
context.become(idlingBehavior)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case default =>
failWithError(s"Invalid packet class received: $default")
}
private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse {
case SocketPane.NextPort(_, _, portNum) =>
val address = gameTestServerAddress.getAddress.getHostAddress
log.info(s"Connecting to ${address.toLowerCase}: $portNum ...")
val response = ConnectToWorldMessage(serverName, address, portNum)
context.become(idlingBehavior)
middlewareActor ! MiddlewareActor.Send(response)
middlewareActor ! MiddlewareActor.Close()
case _ => ()
}
private def handleGamePktDuringLogin(pkt: PlanetSideGamePacket): Unit = {
pkt match {
case LoginMessage(majorVersion, minorVersion, buildDate, username, _, Some(token), revision) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.debug(s"New login UN:$username Token:$token. $clientVersion")
context.become(idlingBehavior)
accountLoginWithToken(token)
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, None, revision) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.debug(s"New login UN:$username. $clientVersion")
context.become(idlingBehavior)
accountLogin(username, password.getOrElse(""))
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
}
}
private def handleGamePktDuringWorldSelect(pkt: PlanetSideGamePacket): Unit = {
pkt match {
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) =>
val sanitizedName = LoginActor.sanitizeServerName(name)
log.info(s"Request to connect to world '$sanitizedName' ...")
context.become(waitingForServerTransferBehavior)
sockets ! SocketPane.GetNextPort("world", context.self)
case _ =>
log.warning(s"Unhandled GamePacket $pkt")
}
}
private def runLoginTest(): Unit = {
import ctx._ import ctx._
val newToken = this.generateToken() val result = for {
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift("PSForever")))
accountOption <- accountsExact.headOption match {
case Some(account) =>
Future.successful(Some(account))
case None =>
Future.successful(None)
}
} yield accountOption
result.onComplete {
case Success(Some(_)) =>
context.become(accountLoginBehavior) // account found
case Success(None) =>
middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Character database not found; stopping ..."))
middlewareActor ! MiddlewareActor.Close()
case Failure(e) =>
log.error(e.getMessage)
middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Encountered login error; stopping ..."))
middlewareActor ! MiddlewareActor.Close()
}
}
private def accountLogin(username: String, password: String): Unit = {
import ctx._
val newToken = LoginActor.generateToken()
val result = for { val result = for {
// backwards compatibility: prefer exact match first, then try lowercase // backwards compatibility: prefer exact match first, then try lowercase
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username))) accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username)))
@ -160,7 +254,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
case None => case None =>
if (Config.app.login.createMissingAccounts) { if (Config.app.login.createMissingAccounts) {
// generate bcrypted passwords // generate bcrypted passwords
val bcryptedPassword = generateNewPassword(username, password) val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds)
val passhash = password.bcryptBounded(bcryptRounds) val passhash = password.bcryptBounded(bcryptRounds)
// save bcrypted password hash to DB // save bcrypted password hash to DB
ctx.run( ctx.run(
@ -201,7 +295,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
if (account.password == "") { if (account.password == "") {
// generate bcrypted password // generate bcrypted password
// use username as provided by the user (db entry could be wrong), that is the way the launcher does it // use username as provided by the user (db entry could be wrong), that is the way the launcher does it
val bcryptedPassword = generateNewPassword(username, password) val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds)
// update account, set password // update account, set password
ctx.run( ctx.run(
query[persistence.Account] query[persistence.Account]
@ -210,31 +304,33 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
loginSuccessfulResponse(username, newToken) loginSuccessfulResponse(username, newToken)
context.become(displayingServerListBehavior)
updateServerListTask = updateServerListTask =
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList)
future future
case (_, false) => case (_, false) =>
loginPwdFailureResponse(username, newToken) loginFailurePasswordResponse(username, newToken)
Future.successful(None) loginFailureAction()
case (true, _) => case (true, _) =>
loginAccountFailureResponse(username, newToken) loginAccountFailureResponse(username, newToken)
Future.successful(None) loginFailureAction()
} }
case None => Future.successful(None) case None =>
loginFailureAction()
} }
} yield login } yield login
result.onComplete { result.onComplete {
case Success(_) => case Success(_) => ()
case Failure(e) => log.error(e.getMessage) case Failure(e) => log.error(e.getMessage)
} }
} }
def accountLoginWithToken(token: String): Unit = { private def accountLoginWithToken(token: String): Unit = {
import ctx._ import ctx._
val newToken = this.generateToken() val newToken = LoginActor.generateToken()
val result = for { val result = for {
accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token))) accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token)))
accountOption <- accountsExact.headOption match { accountOption <- accountsExact.headOption match {
@ -267,33 +363,35 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
_.port -> lift(port) _.port -> lift(port)
) )
) )
loginSuccessfulResponseToken(account.username, token, newToken) loginSuccessfulResponseWithToken(account.username, token, newToken)
context.become(displayingServerListBehavior)
updateServerListTask = updateServerListTask =
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList)
future future
case (_, false) => case (_, false) =>
loginFailureResponseToken(account.username, token, newToken) loginFailureResponseToken(account.username, token, newToken)
Future.successful(None) loginFailureAction()
case (true, _) => case (true, _) =>
loginAccountFailureResponseToken(account.username, token, newToken) loginAccountFailureResponseToken(account.username, token, newToken)
Future.successful(None) loginFailureAction()
} }
case None => Future.successful(None) case None =>
loginFailureAction()
} }
} yield login } yield login
result.onComplete { result.onComplete {
case Success(_) => case Success(_) => ()
case Failure(e) => log.error(e.getMessage) case Failure(e) => log.error(e.getMessage)
} }
} }
def loginSuccessfulResponse(username: String, newToken: String): Unit = { private def loginSuccessfulResponse(username: String, token: String): Unit = {
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.Success, LoginError.Success,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -304,26 +402,21 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginSuccessfulResponseToken(username: String, token: String, newToken: String): Unit = { private def loginSuccessfulResponseWithToken(username: String, token: String, newToken: String): Unit = {
log.info(s"User $username logged in unsing token $token") log.info(s"User $username logged in using token $token")
middlewareActor ! MiddlewareActor.Send( loginSuccessfulResponse(username, newToken)
LoginRespMessage(
newToken,
LoginError.Success,
StationError.AccountActive,
StationSubscriptionStatus.Active,
0,
username,
10001
)
)
} }
def loginPwdFailureResponse(username: String, newToken: String): Unit = { private def loginFailureAction(): Future[Any] = {
context.become(accountLoginBehavior)
Future.successful(None)
}
private def loginFailurePasswordResponse(username: String, token: String): Unit = {
log.warning(s"Failed login to account $username") log.warning(s"Failed login to account $username")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.BadUsernameOrPassword, LoginError.BadUsernameOrPassword,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -334,7 +427,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseToken(token: String, newToken: String): Unit = { private def loginFailureResponseToken(token: String, newToken: String): Unit = {
log.warning(s"Failed login using unknown token $token") log.warning(s"Failed login using unknown token $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -349,7 +442,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { private def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = {
log.warning(s"Failed login using expired token $token") log.warning(s"Failed login using expired token $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -364,11 +457,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponse(username: String, newToken: String): Unit = { private def loginFailureResponse(username: String, token: String): Unit = {
log.warning(s"DB problem username: $username") log.warning(s"DB problem username: $username")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.unk1, LoginError.unk1,
StationError.AccountActive, StationError.AccountActive,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -379,7 +472,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { private def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = {
log.warning(s"DB problem username $username token: $token") log.warning(s"DB problem username $username token: $token")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -394,11 +487,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginAccountFailureResponse(username: String, newToken: String): Unit = { private def loginAccountFailureResponse(username: String, token: String): Unit = {
log.warning(s"Account $username inactive") log.warning(s"Account $username inactive")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
newToken, token,
LoginError.BadUsernameOrPassword, LoginError.BadUsernameOrPassword,
StationError.AccountClosed, StationError.AccountClosed,
StationSubscriptionStatus.Active, StationSubscriptionStatus.Active,
@ -409,7 +502,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { private def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = {
log.warning(s"Account $username inactive token: $token ") log.warning(s"Account $username inactive token: $token ")
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
LoginRespMessage( LoginRespMessage(
@ -424,16 +517,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def generateToken(): String = { private def updateServerList(): Unit = {
val r = new scala.util.Random //todo list of game servers from database, eventually, which is a separation of game server from login server
val sb = new mutable.StringBuilder
for (_ <- 1 to 31) {
sb.append(r.nextPrintableChar())
}
sb.toString
}
def updateServerList(): Unit = {
middlewareActor ! MiddlewareActor.Send( middlewareActor ! MiddlewareActor.Send(
VNLWorldStatusMessage( VNLWorldStatusMessage(
"Welcome to PlanetSide! ", "Welcome to PlanetSide! ",
@ -450,7 +535,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
) )
} }
def failWithError(error: String): Unit = { private def failWithError(error: String): Unit = {
log.error(error) log.error(error)
middlewareActor ! MiddlewareActor.Close() middlewareActor ! MiddlewareActor.Close()
} }

View file

@ -154,6 +154,10 @@ object AvatarActor {
final case class UpdatePurchaseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) final case class UpdatePurchaseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now())
extends Command extends Command
/** rchase time for the use of calculating cooldowns */
final case class UpdateCUDTime(action: String, time: LocalDateTime = LocalDateTime.now())
extends Command
/** Set use time for the use of calculating cooldowns */ /** Set use time for the use of calculating cooldowns */
final case class UpdateUseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command final case class UpdateUseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command
@ -459,7 +463,15 @@ object AvatarActor {
case _ => () case _ => ()
} }
} catch { } catch {
case _: Exception => () case _: Exception =>
val cooldown = LocalDateTime.parse(b)
name match {
case "orbital_strike" if now.compareTo(cooldown.plusMillis(3.hours.toMillis.toInt)) == -1 =>
cooldowns.put(name, cooldown)
case "emp_blast" | "reveal_friendlies" | "reveal_enemies" if now.compareTo(cooldown.plusMillis(20.minutes.toMillis.toInt)) == -1 =>
cooldowns.put(name, cooldown)
case _ => ()
}
} }
case _ => case _ =>
log.warn(s"ignoring invalid cooldown string: '$value'") log.warn(s"ignoring invalid cooldown string: '$value'")
@ -946,12 +958,16 @@ object AvatarActor {
def setBepOnly(avatarId: Long, bep: Long): Future[Long] = { def setBepOnly(avatarId: Long, bep: Long): Future[Long] = {
import ctx._ import ctx._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Long] = Promise() ctx.transaction { implicit ec =>
val result = ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(bep))) for {
result.onComplete { _ => currBep <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).map(_.bep))
out.completeWith(Future(bep)) .map(_.headOption.getOrElse(0L))
newBep = currBep + bep
_ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(newBep)))
} yield newBep
} }
out.future
} }
def loadExperienceDebt(avatarId: Long): Future[Long] = { def loadExperienceDebt(avatarId: Long): Future[Long] = {
@ -1131,6 +1147,11 @@ class AvatarActor(
var supportExperienceTimer: Cancellable = Default.Cancellable var supportExperienceTimer: Cancellable = Default.Cancellable
var experienceDebt: Long = 0L var experienceDebt: Long = 0L
private def setSession(newSession: Session): Unit = {
session = Some(newSession)
_avatar = Option(newSession.avatar)
}
def avatar: Avatar = _avatar.get def avatar: Avatar = _avatar.get
def avatar_=(avatar: Avatar): Unit = { def avatar_=(avatar: Avatar): Unit = {
@ -1156,7 +1177,7 @@ class AvatarActor(
postLoginBehaviour() postLoginBehaviour()
case SetSession(newSession) => case SetSession(newSession) =>
session = Some(newSession) setSession(newSession)
postLoginBehaviour() postLoginBehaviour()
case other => case other =>
@ -1176,7 +1197,7 @@ class AvatarActor(
Behaviors Behaviors
.receiveMessage[Command] { .receiveMessage[Command] {
case SetSession(newSession) => case SetSession(newSession) =>
session = Some(newSession) setSession(newSession)
Behaviors.same Behaviors.same
case SetLookingForSquad(lfs) => case SetLookingForSquad(lfs) =>
@ -1329,7 +1350,7 @@ class AvatarActor(
Behaviors Behaviors
.receiveMessagePartial[Command] { .receiveMessagePartial[Command] {
case SetSession(newSession) => case SetSession(newSession) =>
session = Some(newSession) setSession(newSession)
Behaviors.same Behaviors.same
case ReplaceAvatar(newAvatar) => case ReplaceAvatar(newAvatar) =>
@ -1540,6 +1561,21 @@ class AvatarActor(
} }
Behaviors.same Behaviors.same
case UpdateCUDTime(action, time) =>
var theTimes = avatar.cooldowns.purchase
var updateTheTimes: Boolean = false
Avatar.cudCooldowns.get(action) match {
case Some(_) =>
//only send for items with cooldowns
updateTheTimes = true
theTimes = theTimes.updated(action, time)
case _ => ()
}
if (updateTheTimes) {
avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = theTimes)))
}
Behaviors.same
case UpdateUseTime(definition, time) => case UpdateUseTime(definition, time) =>
if (!Avatar.useCooldowns.contains(definition)) { if (!Avatar.useCooldowns.contains(definition)) {
log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown") log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown")
@ -1838,7 +1874,7 @@ class AvatarActor(
implantTimers.foreach(_.cancel()) implantTimers.foreach(_.cancel())
supportExperienceTimer.cancel() supportExperienceTimer.cancel()
if (supportExperiencePool > 0) { if (supportExperiencePool > 0) {
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool) AvatarActor.setBepOnly(avatar.id, supportExperiencePool)
} }
AvatarActor.saveAvatarData(avatar) AvatarActor.saveAvatarData(avatar)
saveLockerFunc() saveLockerFunc()
@ -2646,6 +2682,43 @@ class AvatarActor(
} }
if (keysToDrop.nonEmpty) { if (keysToDrop.nonEmpty) {
val cdown = avatar.cooldowns val cdown = avatar.cooldowns
val cud = Array("orbital_strike", "emp_blast", "reveal_friendlies", "reveal_enemies")
keysToDrop.foreach { key =>
if (cud.contains(key)) {
avatar
.cooldowns
.purchase
.find { case (name, _) => name.equals(key) }
.flatMap { case (name, purchaseTime) =>
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
Avatar
.cudCooldowns
.find(_._1.equals(name))
.collect {
case (action, cooldown) =>
(action, cooldown.toSeconds - secondsSincePurchase)
}
.orElse {
None
}
.collect {
case (action, remainingTime) if remainingTime > 0 =>
val convertTime = remainingTime * 1000
keysToDrop = keysToDrop.filterNot(_ == action)
action match {
case "orbital_strike" =>
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 60, convertTime))
case "emp_blast" =>
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 59, convertTime))
case "reveal_enemies" =>
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 58, convertTime))
case "reveal_friendlies" =>
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 57, convertTime))
}
}
}
}
}
avatarCopy(avatar.copy(cooldowns = cdown.copy(purchase = cdown.purchase.removedAll(keysToDrop)))) avatarCopy(avatar.copy(cooldowns = cdown.copy(purchase = cdown.purchase.removedAll(keysToDrop))))
} }
} }
@ -2877,7 +2950,7 @@ class AvatarActor(
def setBep(bep: Long, modifier: ExperienceType): Unit = { def setBep(bep: Long, modifier: ExperienceType): Unit = {
import ctx._ import ctx._
AvatarActor.setBepOnly(avatar.id, bep).onComplete { AvatarActor.setBepOnly(avatar.id, bep).onComplete {
case Success(_) => case Success(newBep) =>
val sess = session.get val sess = session.get
val zone = sess.zone val zone = sess.zone
val zoneId = zone.id val zoneId = zone.id
@ -2886,10 +2959,10 @@ class AvatarActor(
val pguid = player.GUID val pguid = player.GUID
val localModifier = modifier val localModifier = modifier
val current = BattleRank.withExperience(avatar.bep).value val current = BattleRank.withExperience(avatar.bep).value
val next = BattleRank.withExperience(bep).value val next = BattleRank.withExperience(newBep).value
val br24 = BattleRank.BR24.value val br24 = BattleRank.BR24.value
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier)) sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, newBep, localModifier))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep)) events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, newBep))
if (current < br24 && next >= br24 || current >= br24 && next < br24) { if (current < br24 && next >= br24 || current >= br24 && next < br24) {
setCosmetics(Set()).onComplete { _ => setCosmetics(Set()).onComplete { _ =>
val evts = events val evts = events
@ -2901,7 +2974,7 @@ class AvatarActor(
// when the level is reduced, take away any implants over the implant slot limit // when the level is reduced, take away any implants over the implant slot limit
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(newBep).implantSlots && implant.isDefined) {
ctx ctx
.run( .run(
query[persistence.Implant] query[persistence.Implant]
@ -2920,7 +2993,7 @@ class AvatarActor(
implant implant
} }
} }
avatar = avatar.copy(bep = bep, implants = implants) avatar = avatar.copy(bep = newBep, implants = implants)
case Failure(exception) => case Failure(exception) =>
log.error(exception)("db failure") log.error(exception)("db failure")
} }
@ -2943,11 +3016,11 @@ class AvatarActor(
} }
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = { def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
setBep(avatar.bep + bep, ExperienceType.Support) setBep(bep, ExperienceType.Support)
} }
def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = { def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = {
setBep(avatar.bep + bep, ExperienceType.Support) setBep(bep, ExperienceType.Support)
supportExperiencePool = supportExperiencePool - bep supportExperiencePool = supportExperiencePool - bep
if (supportExperiencePool > 0) { if (supportExperiencePool > 0) {
resetSupportExperienceTimer(bep, delayBy) resetSupportExperienceTimer(bep, delayBy)
@ -2981,7 +3054,7 @@ class AvatarActor(
} }
private def setBepAction(modifier: ExperienceType)(value: Long): Unit = { private def setBepAction(modifier: ExperienceType)(value: Long): Unit = {
setBep(avatar.bep + value, modifier) setBep(value, modifier)
} }
private def setSupportAction(value: Long): Unit = { private def setSupportAction(value: Long): Unit = {
@ -3032,8 +3105,10 @@ class AvatarActor(
) )
} }
if (exp > 0L) { if (exp > 0L) {
setBep(avatar.bep + exp, msg) setBep(exp, msg)
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp) zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp)
zone.AvatarEvents ! AvatarServiceMessage(
player.Name, AvatarAction.ShareKillExperienceWithSquad(player, exp))
} }
} }

View file

@ -160,7 +160,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (mode != newMode) { if (mode != newMode) {
logic.switchFrom(data.session) logic.switchFrom(data.session)
mode = newMode mode = newMode
logic = mode.setup(data) logic = newMode.setup(data)
} }
logic.switchTo(data.session) logic.switchTo(data.session)
} }
@ -517,7 +517,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case packet: WeaponLazeTargetPositionMessage => case packet: WeaponLazeTargetPositionMessage =>
logic.shooting.handleWeaponLazeTargetPosition(packet) logic.shooting.handleWeaponLazeTargetPosition(packet)
case _: UplinkRequest => () case packet: UplinkRequest =>
logic.shooting.handleUplinkRequest(packet)
case packet: HitMessage => case packet: HitMessage =>
logic.shooting.handleDirectHit(packet) logic.shooting.handleDirectHit(packet)

View file

@ -0,0 +1,577 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.SessionActor
import net.psforever.actors.session.normal.NormalMode
import net.psforever.actors.session.support.AvatarHandlerFunctions
import net.psforever.login.WorldSession.PutLoadoutEquipmentInInventory
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.containable.ContainableBehavior
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
import net.psforever.services.avatar.AvatarServiceResponse
import net.psforever.types.ImplantType
//
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData}
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.zones.Zoning
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game.{ArmorChangedMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage}
import net.psforever.services.avatar.AvatarResponse
import net.psforever.services.Service
import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.util.Config
object AvatarHandlerLogic {
def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = {
new AvatarHandlerLogic(ops, ops.context)
}
}
class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/**
* na
* @param toChannel na
* @param guid na
* @param reply na
*/
def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
val resolvedPlayerGuid = if (player != null && player.HasGUID) {
player.GUID
} else {
Service.defaultPlayerGUID
}
val isNotSameTarget = resolvedPlayerGuid != guid
val isSameTarget = !isNotSameTarget
reply match {
/* special messages */
case AvatarResponse.TeardownConnection() if player.spectator =>
context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode)
context.self.forward(AvatarServiceResponse(toChannel, guid, reply))
case AvatarResponse.TeardownConnection() =>
context.self ! SessionActor.SetMode(NormalMode)
context.self.forward(AvatarServiceResponse(toChannel, guid, reply))
/* really common messages (very frequently, every life) */
case pstate @ AvatarResponse.PlayerState(
pos,
vel,
yaw,
pitch,
yawUpper,
_,
isCrouching,
isJumping,
jumpThrust,
isCloaking,
isNotRendered,
canSeeReallyFar
) if isNotSameTarget =>
val pstateToSave = pstate.copy(timestamp = 0)
val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match {
case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting)
case _ => (None, 0L, Vector3.Zero, false, None)
}
val drawConfig = Config.app.game.playerDraw //m
val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m
val ourPosition = player.Position //xyz
val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m
val inDrawableRange = currentDistance <= maxRange
val now = System.currentTimeMillis() //ms
if (
sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing &&
!isNotRendered && inDrawableRange
) {
//conditions where visibility is assured
val durationSince = now - lastTime //ms
lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange
lazy val targetDelay = {
val populationOver = math.max(
0,
sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold
)
val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m
val adjustedDistance = currentDistance + distanceAdjustment //sq.m
drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match {
case -1 => 1
case index => drawConfig.delays(index)
}
} //ms
if (!wasVisible ||
!previouslyInDrawableRange ||
durationSince > drawConfig.delayMax ||
(!lastMsg.contains(pstateToSave) &&
(canSeeReallyFar ||
currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
sessionLogic.general.canSeeReallyFar ||
durationSince > targetDelay
)
)
) {
//must draw
sendResponse(
PlayerStateMessage(
guid,
pos,
vel,
yaw,
pitch,
yawUpper,
timestamp = 0, //is this okay?
isCrouching,
isJumping,
jumpThrust,
isCloaking
)
)
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
} else {
//is visible, but skip reinforcement
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime))
}
} else {
//conditions where the target is not currently visible
if (wasVisible) {
//the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance
val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat
sendResponse(
PlayerStateMessage(
guid,
Vector3(1f, lat, 1f),
vel=None,
facingYaw=0f,
facingPitch=0f,
facingYawUpper=0f,
timestamp=0, //is this okay?
is_cloaked = isCloaking
)
)
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
} else {
//skip drawing altogether
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime))
}
}
case AvatarResponse.AvatarImplant(ImplantAction.Add, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Add, implant_slot, 7))
//second wind does not normally load its icon into the shortcut hotbar
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.isEmpty}
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, Some(ImplantType.SecondWind.shortcut)))
}
case AvatarResponse.AvatarImplant(ImplantAction.Remove, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Remove, implant_slot, value))
//second wind does not normally unload its icon from the shortcut hotbar
val shortcut = {
val imp = ImplantType.SecondWind.shortcut
net.psforever.objects.avatar.Shortcut(imp.code, imp.tile) //case class
}
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.contains(shortcut) }
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, None))
}
case AvatarResponse.AvatarImplant(action, implant_slot, value) =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, action, implant_slot, value))
case AvatarResponse.ObjectHeld(slot, _)
if isSameTarget && player.VisibleSlots.contains(slot) =>
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
//Stop using proximity terminals if player unholsters a weapon
continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect {
case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term)
}
if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
sessionLogic.zoning.spawn.stopDeconstructing()
}
case AvatarResponse.ObjectHeld(slot, _)
if isSameTarget && slot > -1 =>
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
case AvatarResponse.ObjectHeld(_, _)
if isSameTarget => ()
case AvatarResponse.ObjectHeld(_, previousSlot) =>
sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false))
case AvatarResponse.ChangeFireState_Start(weaponGuid)
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
val entry = ops.lastSeenStreamMessage(guid.guid)
ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid)))
case AvatarResponse.ChangeFireState_Start(weaponGuid)
if isNotSameTarget =>
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
case AvatarResponse.ChangeFireState_Stop(weaponGuid)
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
val entry = ops.lastSeenStreamMessage(guid.guid)
ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None))
case AvatarResponse.ChangeFireState_Stop(weaponGuid)
if isNotSameTarget =>
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget =>
sendResponse(pkt)
case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget =>
sendResponse(pkt)
case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget =>
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) =>
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
sendResponse(HitHint(sourceGuid, guid))
sessionLogic.zoning.CancelZoningProcess()
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer
sendResponse(DestroyMessage(victim, killer, weapon, pos))
case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk))
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
sessionLogic.terminals.lastTerminalOrderFulfillment = true
AvatarActor.savePlayerData(player)
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
sessionLogic.terminals.lastTerminalOrderFulfillment = true
case AvatarResponse.ChangeExosuit(
target,
armor,
exosuit,
subtype,
_,
maxhand,
oldHolsters,
holsters,
oldInventory,
inventory,
drop,
delete
) if resolvedPlayerGuid == target =>
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
//happening to this player
//cleanup
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
(oldHolsters ++ oldInventory ++ delete).foreach {
case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0))
}
//functionally delete
delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) }
//redraw
if (maxhand) {
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
0
))
}
//draw free hand
player.FreeHand.Equipment.foreach { obj =>
val definition = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
ObjectCreateMessageParent(target, Player.FreeHandSlot),
definition.Packet.DetailedConstructorData(obj).get
)
)
}
//draw holsters and inventory
(holsters ++ inventory).foreach {
case InventoryItem(obj, index) =>
val definition = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
ObjectCreateMessageParent(target, index),
definition.Packet.DetailedConstructorData(obj).get
)
)
}
DropLeftovers(player)(drop)
case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) =>
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
//happening to some other player
sendResponse(ObjectHeldMessage(target, slot, unk1 = false))
//cleanup
val dropPred = ContainableBehavior.DropPredicate(player)
val deleteFromDrop = drop.filterNot(dropPred)
(oldHolsters ++ delete ++ deleteFromDrop.map(f =>(f.obj, f.GUID)))
.distinctBy(_._2)
.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
//draw holsters
holsters.foreach {
case InventoryItem(obj, index) =>
val definition = obj.Definition
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
obj.GUID,
ObjectCreateMessageParent(target, index),
definition.Packet.ConstructorData(obj).get
)
)
}
case AvatarResponse.ChangeLoadout(
target,
armor,
exosuit,
subtype,
_,
maxhand,
oldHolsters,
holsters,
oldInventory,
inventory,
drops
) if resolvedPlayerGuid == target =>
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
//happening to this player
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
//cleanup
(oldHolsters ++ oldInventory).foreach {
case (obj, objGuid) =>
sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
}
drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
//redraw
if (maxhand) {
sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong))
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
slot = 0
))
}
(holsters ++ inventory).foreach { case InventoryItem(item, slot) =>
TaskWorkflow.execute(PutLoadoutEquipmentInInventory(player)(item, slot))
}
DropLeftovers(player)(drops)
case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
//redraw handled by callbacks
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
//happening to some other player
sendResponse(ObjectHeldMessage(target, slot, unk1=false))
//cleanup
oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
case AvatarResponse.UseKit(kguid, kObjId) =>
sendResponse(
UseItemMessage(
resolvedPlayerGuid,
kguid,
resolvedPlayerGuid,
unk2 = 4294967295L,
unk3 = false,
unk4 = Vector3.Zero,
unk5 = Vector3.Zero,
unk6 = 126,
unk7 = 0, //sequence time?
unk8 = 137,
kObjId
)
)
sendResponse(ObjectDeleteMessage(kguid, unk1=0))
case AvatarResponse.KitNotUsed(_, "") =>
sessionLogic.general.kitToBeUsed = None
case AvatarResponse.KitNotUsed(_, msg) =>
sessionLogic.general.kitToBeUsed = None
sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
case AvatarResponse.SendResponse(msg) =>
sendResponse(msg)
case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid =>
sendResponse(msg)
/* common messages (maybe once every respawn) */
case AvatarResponse.Reload(itemGuid)
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
case AvatarResponse.Killed(_, mount) =>
//pure logic
sessionLogic.shooting.shotsWhileDead = 0
sessionLogic.zoning.CancelZoningProcess()
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
sessionLogic.zoning.zoningStatus = Zoning.Status.None
continent.GUID(mount).collect {
case obj: Vehicle if obj.Destroyed =>
ops.killedWhileMounted(obj, resolvedPlayerGuid)
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
sessionLogic.general.unaccessContainer(obj)
case obj: Vehicle =>
ops.killedWhileMounted(obj, resolvedPlayerGuid)
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
case obj: PlanetSideGameObject with Mountable with Container if obj.Destroyed =>
ops.killedWhileMounted(obj, resolvedPlayerGuid)
sessionLogic.general.unaccessContainer(obj)
case obj: PlanetSideGameObject with Mountable with Container =>
ops.killedWhileMounted(obj, resolvedPlayerGuid)
case obj: PlanetSideGameObject with Mountable =>
ops.killedWhileMounted(obj, resolvedPlayerGuid)
}
//player state changes
sessionLogic.general.dropSpecialSlotItem()
sessionLogic.general.toggleMaxSpecialState(enable = false)
player.FreeHand.Equipment.foreach(DropEquipmentFromInventory(player)(_))
AvatarActor.updateToolDischargeFor(avatar)
AvatarActor.savePlayerLocation(player)
ops.revive(player.GUID)
avatarActor ! AvatarActor.InitializeImplants
//render
CustomerServiceRepresentativeMode.renderPlayer(sessionLogic, continent, player)
case AvatarResponse.Release(tplayer) if isNotSameTarget =>
sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer)
case AvatarResponse.Revive(revivalTargetGuid)
if resolvedPlayerGuid == revivalTargetGuid =>
ops.revive(revivalTargetGuid)
/* uncommon messages (utility, or once in a while) */
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
sendResponse(ChangeAmmoMessage(weapon_guid, 1))
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
if isNotSameTarget =>
ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget =>
sendResponse(ChangeFireModeMessage(itemGuid, mode))
case AvatarResponse.ConcealPlayer() =>
sendResponse(GenericObjectActionMessage(guid, code=9))
case AvatarResponse.EnvironmentalDamage(_, _, _) =>
//TODO damage marker?
sessionLogic.zoning.CancelZoningProcess()
case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
sendResponse(pkt)
case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget =>
sendResponse(ObjectDeleteMessage(itemGuid, unk))
/* rare messages */
case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget =>
sendResponse(SetEmpireMessage(objectGuid, faction))
case AvatarResponse.DropSpecialItem() =>
sessionLogic.general.dropSpecialSlotItem()
case AvatarResponse.OxygenState(player, vehicle) =>
sendResponse(OxygenStateMessage(
DrowningTarget(player.guid, player.progress, player.state),
vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) }
))
case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget =>
sendResponse(pkt)
case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget =>
sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid))
case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) =>
sendResponse(
ProjectileStateMessage(
projectileGuid,
projectile.Position,
shot_vel = Vector3.Zero,
projectile.Orientation,
sequence_num=0,
end=true,
hit_target_guid=PlanetSideGUID(0)
)
)
sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2))
case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
sendResponse(GenericActionMessage(mode))
case AvatarResponse.PutDownFDU(target) if isNotSameTarget =>
sendResponse(GenericObjectActionMessage(target, code=53))
case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget =>
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(target, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
case AvatarResponse.WeaponDryFire(weaponGuid)
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
continent.GUID(weaponGuid).collect {
case tool: Tool if tool.Magazine == 0 =>
// check that the magazine is still empty before sending WeaponDryFireMessage
// if it has been reloaded since then, other clients will not see it firing
sendResponse(WeaponDryFireMessage(weaponGuid))
}
case _ => ()
}
}
}

View file

@ -0,0 +1,549 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.ActorContext
import net.psforever.actors.session.SessionActor
import net.psforever.actors.session.normal.NormalMode
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable}
import net.psforever.objects.ce.{Deployable, DeployableCategory}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel}
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
import net.psforever.types.{ChatMessageType, PlanetSideEmpire}
import net.psforever.zones.Zones
import scala.util.Success
object ChatLogic {
def apply(ops: ChatOperations): ChatLogic = {
new ChatLogic(ops, ops.context)
}
}
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
ops.transitoryCommandEntered match {
case Some(CMT_TOGGLESPECTATORMODE) =>
//we are transitioning down from csr spectator mode to normal mode, continue to block transitory messages
()
case _ =>
//correct player mode
ops.transitoryCommandEntered = None
}
def sessionLogic: SessionData = ops.sessionLogic
ops.CurrentSpectatorMode = SpectateAsCustomerServiceRepresentativeMode
private var comms: ChatChannel = DefaultChannel
private var seeSpectatorsIn: Option[Zone] = None
def handleChatMsg(message: ChatMsg): Unit = {
import net.psforever.types.ChatMessageType._
val isAlive = if (player != null) player.isAlive else false
(message.messageType, message.recipient.trim, message.contents.trim) match {
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session) => ()
case (CMT_FLY, recipient, contents) =>
ops.commandFly(contents, recipient)
case (CMT_ANONYMOUS, _, _) => ()
case (CMT_TOGGLE_GM, _, contents) =>
customCommandModerator(contents)
case (CMT_CULLWATERMARK, _, contents) =>
ops.commandWatermark(contents)
case (CMT_SPEED, _, contents) =>
ops.commandSpeed(message, contents)
case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive =>
commandToggleSpectatorMode(contents)
case (CMT_RECALL, _, _) =>
ops.commandRecall(session)
case (CMT_INSTANTACTION, _, _) =>
ops.commandInstantAction(session)
case (CMT_QUIT, _, _) =>
ops.commandQuit(session)
case (CMT_SUICIDE, _, _) =>
ops.commandSuicide(session)
case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
ops.commandDestroy(session, message, contents)
case (CMT_SETBASERESOURCES, _, contents) =>
ops.commandSetBaseResources(session, contents)
case (CMT_ZONELOCK, _, contents) =>
ops.commandZoneLock(contents)
case (U_CMT_ZONEROTATE, _, _) =>
ops.commandZoneRotate()
case (CMT_CAPTUREBASE, _, contents) =>
ops.commandCaptureBase(session, message, contents)
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_GMTELL, _, _) =>
ops.commandSend(session, message, comms)
case (CMT_GMBROADCASTPOPUP, _, _) =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_OPEN, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_VOICE, _, contents) =>
ops.commandVoice(session, message, contents, comms)
case (CMT_TELL, _, _) if !player.silenced =>
ops.commandTellOrIgnore(session, message, comms)
case (CMT_BROADCAST, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_PLATOON, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_COMMAND, _, _) =>
ops.commandSendToRecipient(session, message, comms)
case (CMT_NOTE, _, _) =>
ops.commandSend(session, message, comms)
case (CMT_SILENCE, _, _) =>
ops.commandSend(session, message, comms)
case (CMT_SQUAD, _, _) =>
ops.commandSquad(session, message, comms) //todo SquadChannel, but what is the guid
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
ops.commandWho(session)
case (CMT_ZONE, _, contents) =>
ops.commandZone(message, contents)
case (CMT_WARP, _, contents) =>
ops.commandWarp(session, message, contents)
case (CMT_SETBATTLERANK, _, contents) =>
ops.commandSetBattleRank(session, message, contents)
case (CMT_SETCOMMANDRANK, _, contents) =>
ops.commandSetCommandRank(session, message, contents)
case (CMT_ADDBATTLEEXPERIENCE, _, contents) =>
ops.commandAddBattleExperience(message, contents)
case (CMT_ADDCOMMANDEXPERIENCE, _, contents) =>
ops.commandAddCommandExperience(message, contents)
case (CMT_TOGGLE_HAT, _, contents) =>
ops.commandToggleHat(session, message, contents)
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
ops.commandToggleCosmetics(session, message, contents)
case (CMT_ADDCERTIFICATION, _, contents) =>
ops.commandAddCertification(session, message, contents)
case (CMT_KICK, _, contents) =>
ops.commandKick(session, message, contents)
case _ =>
log.warn(s"Unhandled chat message $message")
}
}
def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
val SetChatFilterMessage(_, _, _) = pkt
}
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
import ChatMessageType._
message.messageType match {
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
ops.commandIncomingSendAllIfOnline(session, message)
case CMT_OPEN =>
ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
case CMT_TELL | U_CMT_TELLFROM |
CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
ops.commandIncomingSend(message)
case CMT_VOICE =>
ops.commandIncomingVoice(session, fromSession, message)
case CMT_SILENCE =>
ops.commandIncomingSilence(session, message)
case _ =>
log.warn(s"Unexpected messageType $message")
}
}
private def customCommandMessages(
message: ChatMsg,
session: Session
): Boolean = {
val contents = message.contents
if (contents.startsWith("!")) {
val (command, params) = ops.cliTokenization(contents.drop(1)) match {
case a :: b => (a, b)
case _ => ("", Seq(""))
}
command match {
case "loc" => ops.customCommandLoc(session, message)
case "suicide" => ops.customCommandSuicide(session)
case "grenade" => ops.customCommandGrenade(session, log)
case "macro" => ops.customCommandMacro(session, params)
case "progress" => ops.customCommandProgress(session, params)
case "whitetext" => ops.customCommandWhitetext(session, params)
case "list" => ops.customCommandList(session, params, message)
case "ntu" => ops.customCommandNtu(session, params)
case "zonerotate" => ops.customCommandZonerotate(params)
case "nearby" => ops.customCommandNearby(session)
case "togglespectators" => customCommandToggleSpectators(params)
case "showspectators" => customCommandShowSpectators()
case "hidespectators" => customCommandHideSpectators()
case "sayspectator" => customCommandSpeakAsSpectator(params, message)
case "setempire" => customCommandSetEmpire(params)
case "weaponlock" => customCommandZoneWeaponUnlock(session, params)
case _ =>
// command was not handled
sendResponse(
ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
true
}
} else {
false
}
}
private def commandToggleSpectatorMode(contents: String): Unit = {
contents.toLowerCase() match {
case "on" | "o" | "" if !player.spectator =>
context.self ! SessionActor.SetMode(SpectateAsCustomerServiceRepresentativeMode)
case "off" | "of" if player.spectator =>
context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode)
case _ => ()
}
}
private def customCommandModerator(contents: String): Boolean = {
if (sessionLogic.zoning.maintainInitialGmState) {
sessionLogic.zoning.maintainInitialGmState = false
true
} else {
ops.transitoryCommandEntered
.collect {
case CMT_TOGGLE_GM => true
case CMT_TOGGLESPECTATORMODE => false
}
.getOrElse {
contents.toLowerCase() match {
case "off" | "of" if player.spectator =>
ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE)
context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode)
context.self ! SessionActor.SetMode(NormalMode)
true
case "off" | "of" =>
ops.transitoryCommandEntered = Some(CMT_TOGGLE_GM)
context.self ! SessionActor.SetMode(NormalMode)
true
case _ =>
false
}
}
}
}
private def customCommandToggleSpectators(contents: Seq[String]): Boolean = {
contents
.headOption
.map(_.toLowerCase())
.collect {
case "on" | "o" | "" if !seeSpectatorsIn.contains(continent) =>
customCommandShowSpectators()
case "off" | "of" if seeSpectatorsIn.contains(continent) =>
customCommandHideSpectators()
case _ => ()
}
true
}
private def customCommandShowSpectators(): Boolean = {
val channel = player.Name
val events = continent.AvatarEvents
seeSpectatorsIn = Some(continent)
events ! Service.Join(s"spectator")
continent
.AllPlayers
.filter(_.spectator)
.foreach { spectator =>
val guid = spectator.GUID
val definition = spectator.Definition
events ! AvatarServiceMessage(
channel,
AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(spectator).get, None)
)
}
true
}
private def customCommandHideSpectators(): Boolean = {
val channel = player.Name
val events = continent.AvatarEvents
seeSpectatorsIn = None
events ! Service.Leave(Some("spectator"))
continent
.AllPlayers
.filter(_.spectator)
.foreach { spectator =>
val guid = spectator.GUID
events ! AvatarServiceMessage(
channel,
AvatarAction.ObjectDelete(guid, guid)
)
}
true
}
private def customCommandSpeakAsSpectator(params: Seq[String], message: ChatMsg): Boolean = {
comms = SpectatorChannel
handleChatMsg(message.copy(contents = params.mkString(" ")))
comms = DefaultChannel
true
}
private def customCommandSetEmpire(params: Seq[String]): Boolean = {
var postUsage: Boolean = false
val (entityOpt, foundFaction) = (params.headOption, params.lift(1)) match {
case (Some(guid), Some(faction)) if guid.toIntOption.nonEmpty =>
try {
(continent.GUID(guid.toInt), PlanetSideEmpire.apply(faction))
} catch {
case _: Exception =>
(None, PlanetSideEmpire.NEUTRAL)
}
case (Some(guid), None) if guid.toIntOption.nonEmpty =>
(continent.GUID(guid.toInt), player.Faction)
case _ =>
postUsage = true
(None, PlanetSideEmpire.NEUTRAL)
}
entityOpt
.collect {
case f: FactionAffinity if f.Faction != foundFaction && foundFaction != PlanetSideEmpire.NEUTRAL => f
}
.collect {
case o: TurretDeployable
if o.Definition.DeployCategory == DeployableCategory.FieldTurrets =>
//remove prior turret and construct new one
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
o.Actor ! Deployable.Deconstruct(Some(2.seconds))
sessionLogic.general.handleDeployObject(
continent,
GlobalDefinitions.PortableMannedTurret(foundFaction).Item,
o.Position,
o.Orientation,
o.WhichSide,
foundFaction
).onComplete {
case Success(obj2) => sendResponse(ChatMsg(ChatMessageType.UNK_227, s"${obj2.GUID.guid}"))
case _ => ()
}
true
case o: Deployable =>
o.Faction = foundFaction
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, o.GUID, foundFaction)
)
true
case o: Building =>
ops.commandCaptureBaseProcessResults(Some(Seq(o)), Some(foundFaction), Some(1))
true
case o: PlanetSideServerObject with Hackable =>
o.Actor ! CommonMessages.Hack(player, o)
true
case o: PlanetSideGameObject with FactionAffinity =>
o.Faction = foundFaction
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.SetEmpire(Service.defaultPlayerGUID, o.GUID, foundFaction)
)
true
}
.getOrElse {
if (postUsage) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "!setempire guid [faction]"))
} else if (entityOpt.nonEmpty) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "set empire entity not supported"))
} else {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "set empire entity not found"))
}
true
}
}
def customCommandZoneWeaponUnlock(session: Session, params: Seq[String]): Boolean = {
val usageMessage: Boolean = params.exists(_.matches("--help")) || params.exists(_.matches("-h"))
val formattedParams = ops.cliCommaSeparatedParams(params)
//handle params
val (zoneList, verifiedZones, factionList, verifiedFactions, stateOpt) = (formattedParams.headOption, formattedParams.lift(1), formattedParams.lift(2)) match {
case _ if usageMessage =>
(Nil, Nil, Nil, Nil, None)
case (None, None, None) =>
(
Seq(session.zone.id),
Seq(session.zone),
PlanetSideEmpire.values.map(_.toString()).toSeq,
PlanetSideEmpire.values.toSeq,
Some(true)
)
case (Some(zoneOrFaction), Some(factionOrZone), stateOpt) =>
val factionOrZoneSplit = factionOrZone.split(",").toSeq
val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq
val tryToFactions = factionOrZoneSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s)))
if (tryToFactions.isEmpty) {
(
factionOrZoneSplit,
customCommandZoneParse(factionOrZoneSplit),
zoneOrFactionSplit,
zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))),
customCommandOnOffStateOrNone(stateOpt)
)
} else {
(
zoneOrFactionSplit,
customCommandZoneParse(zoneOrFactionSplit),
factionOrZoneSplit,
tryToFactions,
customCommandOnOffStateOrNone(stateOpt)
)
}
case (Some(zoneOrFaction), stateOpt, None) =>
val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq
val tryToFactions = zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s)))
if (tryToFactions.isEmpty) {
(
zoneOrFactionSplit,
customCommandZoneParse(zoneOrFactionSplit),
PlanetSideEmpire.values.map(_.toString()).toSeq,
PlanetSideEmpire.values.toSeq,
customCommandOnOffStateOrNone(stateOpt)
)
} else {
(
Seq(session.zone.id),
Seq(session.zone),
zoneOrFactionSplit,
tryToFactions,
customCommandOnOffStateOrNone(stateOpt)
)
}
case (stateOpt, None, None) =>
(
Seq(session.zone.id),
Seq(session.zone),
PlanetSideEmpire.values.map(_.toString()).toSeq,
PlanetSideEmpire.values.toSeq,
customCommandOnOffState(stateOpt)
)
}
//resolve
if (usageMessage) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "!weaponlock [zone[,...]] [faction[,...]] [o[n]|of[f]]"))
} else if (zoneList.isEmpty || verifiedZones.isEmpty || zoneList.size != verifiedZones.size) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "some zones can not be verified"))
} else if (factionList.isEmpty || verifiedFactions.isEmpty || factionList.size != verifiedFactions.size) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "some factions can not be verified"))
} else if (stateOpt.isEmpty) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "state must be on or off"))
} else {
val state = !stateOpt.get
verifiedZones.foreach { zone =>
val events = zone.AvatarEvents
val zoneId = zone.id
//val reloadZoneMsg = AvatarAction.ReloadZone(zone)
zone
.UpdateLiveFireAllowed(state, verifiedFactions)
.foreach {
case (_, false, _) => ()
case (faction, true, _) =>
//events ! AvatarServiceMessage(s"$faction", reloadZoneMsg)
}
}
}
true
}
private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = {
stateOpt match {
case None =>
Some(true)
case _ =>
customCommandOnOffState(stateOpt)
}
}
private def customCommandOnOffState(stateOpt: Option[String]): Option[Boolean] = {
stateOpt match {
case Some("o") | Some("on") =>
Some(false)
case Some("of") | Some("off") =>
Some(true)
case _ =>
None
}
}
def customCommandZoneParse(potentialZones: Seq[String]): Seq[Zone] = {
potentialZones.flatten { potentialZone =>
if (potentialZone.toIntOption.nonEmpty) {
val xInt = potentialZone.toInt
Zones.zones.find(_.Number == xInt)
} else {
Zones.zones.find(z => z.id.equals(potentialZone))
}
}
}
override def stop(): Unit = {
super.stop()
seeSpectatorsIn.foreach(_ => customCommandHideSpectators())
seeSpectatorsIn = None
}
}

View file

@ -0,0 +1,204 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import net.psforever.actors.session.support.{ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.objects.{Deployables, PlanetSideGameObject, Player, Session, Vehicle}
import net.psforever.objects.avatar.Certification
import net.psforever.objects.serverobject.ServerObject
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage}
import net.psforever.packet.game.objectcreate.RibbonBars
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.{CustomerServiceChannel, SpectatorChannel}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{ChatMessageType, MeritCommendation, PlanetSideGUID}
class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse)
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
val vehicleResponse: VehicleHandlerFunctions = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations)
private var oldRibbons: RibbonBars = RibbonBars()
private var oldCertifications : Set[Certification] = Set()
override def switchTo(session: Session): Unit = {
val player = session.player
val avatar = session.avatar
val continent = session.zone
//
data.zoning.displayZoningMessageWhenCancelled = false
if (oldCertifications.isEmpty) {
oldCertifications = avatar.certifications
oldRibbons = avatar.decoration.ribbonBars
val newAvatar = avatar.copy(
certifications = Certification.values.toSet,
decoration = avatar.decoration.copy(ribbonBars = RibbonBars(
MeritCommendation.CSAppreciation,
MeritCommendation.Loser,
MeritCommendation.Loser,
MeritCommendation.CSAppreciation
))
)
player.avatar = newAvatar
data.session = session.copy(avatar = newAvatar, player = player)
Deployables.InitializeDeployableQuantities(newAvatar)
}
requireDismount(continent, player)
data.keepAlivePersistenceFunc = keepAlivePersistanceCSR
//
CustomerServiceRepresentativeMode.renderPlayer(data, continent, player)
player.allowInteraction = false
if (player.silenced) {
data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0"))
}
data.chat.JoinChannel(SpectatorChannel)
data.chat.JoinChannel(CustomerServiceChannel)
data.sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE ON"))
}
override def switchFrom(session: Session): Unit = {
val player = session.player
val avatar = session.avatar
val continent = session.zone
//
data.zoning.displayZoningMessageWhenCancelled = true
val newAvatar = avatar.copy(
certifications = oldCertifications,
decoration = avatar.decoration.copy(ribbonBars = oldRibbons)
)
oldCertifications = Set()
oldRibbons = RibbonBars()
player.avatar = newAvatar
data.session = session.copy(avatar = newAvatar, player = player)
Deployables.InitializeDeployableQuantities(newAvatar)
//
requireDismount(continent, player)
data.keepAlivePersistenceFunc = data.keepAlivePersistence
//
CustomerServiceRepresentativeMode.renderPlayer(data, continent, player)
player.allowInteraction = true
data.chat.LeaveChannel(SpectatorChannel)
data.chat.LeaveChannel(CustomerServiceChannel)
data.sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE OFF"))
}
private def requireDismount(zone: Zone, player: Player): Unit = {
data.vehicles.GetMountableAndSeat(None, player, zone) match {
case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 =>
data.vehicles.ServerVehicleOverrideStop(obj)
obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat
obj.Actor ! Mountable.TryDismount(player, seatNum)
player.VehicleSeated = None
case (Some(obj), Some(seatNum)) =>
obj.Actor ! Mountable.TryDismount(player, seatNum)
player.VehicleSeated = None
case _ =>
player.VehicleSeated = None
}
}
private def keepAlivePersistanceCSR(): Unit = {
val player = data.player
data.keepAlivePersistence()
topOffHealthOfPlayer(player)
player.allowInteraction = false
topOffHealthOfPlayer(player)
data.continent.GUID(data.player.VehicleSeated)
.collect {
case obj: PlanetSideGameObject with Vitality with BlockMapEntity =>
topOffHealth(obj)
data.updateBlockMap(obj, obj.Position)
obj
}
.getOrElse {
data.updateBlockMap(player, player.Position)
}
}
private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
obj match {
case p: Player => topOffHealthOfPlayer(p)
case v: Vehicle => topOffHealthOfVehicle(v)
case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
case _ => ()
}
}
private def topOffHealthOfPlayer(player: Player): Unit = {
//driver below half health, full heal
val maxHealthOfPlayer = player.MaxHealth.toLong
if (player.Health < maxHealthOfPlayer * 0.5f) {
player.Health = maxHealthOfPlayer.toInt
player.LogActivity(player.ClearHistory().head)
data.sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
data.continent.AvatarEvents ! AvatarServiceMessage(data.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
}
}
private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
topOffHealthOfGeneric(vehicle)
//vehicle shields below half, full shields
val maxShieldsOfVehicle = vehicle.MaxShields.toLong
val shieldsUi = vehicle.Definition.shieldUiAttribute
if (vehicle.Shields < maxShieldsOfVehicle) {
val guid = vehicle.GUID
vehicle.Shields = maxShieldsOfVehicle.toInt
data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
data.continent.VehicleEvents ! VehicleServiceMessage(
data.continent.id,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
)
}
}
private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
//below half health, full heal
val guid = obj.GUID
val maxHealthOf = obj.MaxHealth.toLong
if (obj.Health < maxHealthOf) {
obj.Health = maxHealthOf.toInt
data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
data.continent.VehicleEvents ! VehicleServiceMessage(
data.continent.id,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
)
}
}
}
case object CustomerServiceRepresentativeMode extends PlayerMode {
def setup(data: SessionData): ModeLogic = {
new CustomerServiceRepresentativeMode(data)
}
private[csr] def renderPlayer(data: SessionData, zone: Zone, player: Player): Unit = {
val pguid = player.GUID
val definition = player.Definition
val objectClass = definition.ObjectId
val packet = definition.Packet
data.sendResponse(ObjectCreateDetailedMessage(
objectClass,
pguid,
packet.DetailedConstructorData(player).get
))
data.zoning.spawn.HandleSetCurrentAvatar(player)
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(
pguid,
objectClass,
pguid,
packet.ConstructorData(player).get,
None
))
}
}

View file

@ -0,0 +1,789 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
import net.psforever.objects.avatar.{Avatar, PlayerControl}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.{CommonMessages, ServerObject}
import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.util.Success
object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = {
new GeneralLogic(ops, ops.context)
}
}
class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ }
def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ }
def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { /* intentionally blank */ }
def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = {
val PlayerStateMessageUpstream(
avatarGuid,
pos,
vel,
yaw,
pitch,
yawUpper,
seqTime,
_,
isCrouching,
isJumping,
jumpThrust,
isCloaking,
_,
_
) = pkt
sessionLogic.persist()
sessionLogic.turnCounterFunc(avatarGuid)
sessionLogic.updateBlockMap(player, pos)
//below half health, full heal
val maxHealth = player.MaxHealth.toLong
if (player.Health < maxHealth) {
player.Health = maxHealth.toInt
player.LogActivity(player.ClearHistory().head)
sendResponse(PlanetsideAttributeMessage(avatarGuid, 0, maxHealth))
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 0, maxHealth))
}
//below half stamina, full stamina
val avatar = player.avatar
val maxStamina = avatar.maxStamina
if (avatar.stamina < maxStamina) {
avatarActor ! AvatarActor.RestoreStamina(maxStamina)
sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong))
}
//below half armor, full armor
val maxArmor = player.MaxArmor.toLong
if (player.Armor < maxArmor) {
player.Armor = maxArmor.toInt
sendResponse(PlanetsideAttributeMessage(avatarGuid, 4, maxArmor))
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 4, maxArmor))
}
//expected
val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || isJumping || jumpThrust
if (isMovingPlus) {
if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
sessionLogic.zoning.spawn.stopDeconstructing()
} else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) {
sessionLogic.zoning.CancelZoningProcess()
}
}
ops.fallHeightTracker(pos.z)
// if (isCrouching && !player.Crouching) {
// //dev stuff goes here
// }
player.Position = pos
player.Velocity = vel
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
player.FacingYawUpper = yawUpper
player.Crouching = isCrouching
player.Jumping = isJumping
if (isCloaking && !player.Cloaked) {
sessionLogic.zoning.CancelZoningProcess()
}
player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
maxCapacitorTick(jumpThrust)
if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) {
continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match {
case Some(term: Terminal with ProximityUnit) =>
sessionLogic.terminals.StopUsingProximityUnit(term)
case _ => ()
}
}
ops.accessedContainer match {
// Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle.
// This can happen if PSUM arrives during the mounting process
case Some(container)
if !container.HasGUID && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID) => //just in case
// Ensure we don't close the container if the player is seated in it
val guid = player.GUID
// If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first.
sendResponse(UnuseItemMessage(guid, guid))
ops.unaccessContainer(container)
case _ => ()
}
val channel = if (!player.spectator) {
sessionLogic.updateBlockMap(player, pos)
continent.id
} else {
"spectator"
}
val eagleEye: Boolean = ops.canSeeReallyFar
val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing ||
(player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime)
continent.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.PlayerState(
avatarGuid,
player.Position,
player.Velocity,
yaw,
pitch,
yawUpper,
seqTime,
isCrouching,
isJumping,
jumpThrust,
isCloaking,
isNotVisible,
eagleEye
)
)
sessionLogic.squad.updateSquad()
player.allowInteraction = false
}
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
ops.noVoicedChat(pkt)
}
def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = {
ops.noVoicedChat(pkt)
}
def handleEmote(pkt: EmoteMsg): Unit = {
val EmoteMsg(avatarGuid, emote) = pkt
val pZone = player.Zone
sendResponse(EmoteMsg(avatarGuid, emote))
pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote)))
}
pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote)))
}
}
def handleDropItem(pkt: DropItemMessage): Unit = {
ops.handleDropItem(pkt) match {
case GeneralOperations.ItemDropState.Dropped =>
sessionLogic.zoning.CancelZoningProcess()
case _ => ()
}
}
def handlePickupItem(pkt: PickupItemMessage): Unit = {
ops.handlePickupItem(pkt) match {
case GeneralOperations.ItemPickupState.PickedUp =>
sessionLogic.zoning.CancelZoningProcess()
case _ => ()
}
}
def handleObjectHeld(pkt: ObjectHeldMessage): Unit = {
val ObjectHeldMessage(_, heldHolsters, _) = pkt
player.Actor ! PlayerControl.ObjectHeld(heldHolsters)
}
def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* no stamina loss */ }
def handleZipLine(pkt: ZipLineMessage): Unit = {
ops.handleZipLine(pkt) match {
case GeneralOperations.ZiplineBehavior.Teleporter | GeneralOperations.ZiplineBehavior.Zipline =>
sessionLogic.zoning.CancelZoningProcess()
case _ =>
()
}
}
def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = {
val RequestDestroyMessage(objectGuid) = pkt
//make sure this is the correct response for all cases
sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match {
case Some(vehicle: Vehicle) =>
vehicle.Actor ! Vehicle.Deconstruct()
case Some(obj: Projectile) =>
if (!obj.isResolved) {
obj.Miss()
}
continent.Projectile ! ZoneProjectile.Remove(objectGuid)
case Some(obj: BoomerTrigger) =>
if (ops.findEquipmentToDelete(objectGuid, obj)) {
continent.GUID(obj.Companion) match {
case Some(boomer: BoomerDeployable) =>
boomer.Trigger = None
boomer.Actor ! Deployable.Deconstruct()
case Some(thing) =>
log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing")
case None => ()
}
}
case Some(obj: Deployable) =>
obj.Actor ! Deployable.Deconstruct()
case Some(obj: Equipment) =>
ops.findEquipmentToDelete(objectGuid, obj)
case Some(obj: Player) if obj.isBackpack =>
obj.Position = Vector3.Zero
continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.ClearSpecific(List(obj), continent))
case Some(obj: Player) =>
sessionLogic.general.suicide(obj)
case Some(_) => ()
case None => ()
}
}
def handleMoveItem(pkt: MoveItemMessage): Unit = {
ops.handleMoveItem(pkt)
}
def handleLootItem(pkt: LootItemMessage): Unit = {
ops.handleLootItem(pkt)
}
def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
ops.handleAvatarImplant(pkt) match {
case GeneralOperations.ImplantActivationBehavior.Activate | GeneralOperations.ImplantActivationBehavior.Deactivate =>
sessionLogic.zoning.CancelZoningProcess()
case GeneralOperations.ImplantActivationBehavior.NotFound =>
log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in ${pkt.implantSlot}")
case _ => ()
}
}
def handleUseItem(pkt: UseItemMessage): Unit = {
val equipment = ops.findContainedEquipment(pkt.item_used_guid) match {
case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) =>
sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption
case (Some(_), a) =>
a.headOption
case _ =>
None
}
sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
case Some(door: Door) =>
handleUseDoor(door, equipment)
case Some(resourceSilo: ResourceSilo) =>
ops.handleUseResourceSilo(resourceSilo, equipment)
case Some(panel: IFFLock) =>
ops.handleUseGeneralEntity(panel, equipment)
case Some(obj: Player) =>
ops.handleUsePlayer(obj, equipment, pkt)
case Some(locker: Locker) =>
ops.handleUseLocker(locker, equipment, pkt)
case Some(gen: Generator) =>
ops.handleUseGeneralEntity(gen, equipment)
case Some(mech: ImplantTerminalMech) =>
ops.handleUseGeneralEntity(mech, equipment)
case Some(captureTerminal: CaptureTerminal) =>
ops.handleUseCaptureTerminal(captureTerminal, equipment)
case Some(obj: FacilityTurret) =>
ops.handleUseFacilityTurret(obj, equipment, pkt)
case Some(obj: Vehicle) =>
ops.handleUseVehicle(obj, equipment, pkt)
case Some(terminal: Terminal) =>
ops.handleUseTerminal(terminal, equipment, pkt)
case Some(obj: SpawnTube) =>
ops.handleUseSpawnTube(obj, equipment)
case Some(obj: SensorDeployable) =>
ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TurretDeployable) =>
ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TrapDeployable) =>
ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: ShieldGeneratorDeployable) =>
ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TelepadDeployable) if player.spectator =>
ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystemSecretly)
case Some(obj: Utility.InternalTelepad) if player.spectator =>
ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystemSecretly)
case Some(obj: TelepadDeployable) =>
ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem)
case Some(obj: Utility.InternalTelepad) =>
ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem)
case Some(obj: CaptureFlag) =>
ops.handleUseCaptureFlag(obj)
case Some(_: WarpGate) =>
ops.handleUseWarpGate(equipment)
case Some(obj) =>
ops.handleUseDefaultEntity(obj, equipment)
case None => ()
}
}
def handleUnuseItem(pkt: UnuseItemMessage): Unit = {
val UnuseItemMessage(_, objectGuid) = pkt
sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match {
case Some(obj: Player) =>
ops.unaccessContainer(obj)
sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj)
case Some(obj: Container) =>
// Make sure we don't unload the contents of the vehicle the player is seated in
// An example scenario of this would be closing the trunk contents when rearming at a landing pad
if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) {
ops.unaccessContainer(obj)
}
case _ => ()
}
}
def handleDeployObject(pkt: DeployObjectMessage): Unit = {
if (!player.spectator) {
import scala.concurrent.ExecutionContext.Implicits.global
val DeployObjectMessage(guid, _, pos, orient, _) = pkt
player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match {
case Some(obj: ConstructionItem) =>
sessionLogic.zoning.CancelZoningProcess()
val result = ops.handleDeployObject(continent, obj.AmmoType, pos, orient, player.WhichSide, PlanetSideEmpire.NEUTRAL)
result.onComplete {
case Success(obj) => sendResponse(ChatMsg(ChatMessageType.UNK_227, s"${obj.GUID.guid}"))
case _ => ()
}
case Some(obj) =>
log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!")
case None =>
log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!")
}
}
}
def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = {
val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt
sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match {
case Some(vehicle: Vehicle) =>
vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue)
// Cosmetics options
case Some(_: Player) if attributeType == 106 =>
avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue))
case Some(obj) =>
log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}")
case _ => ()
}
}
def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = {
val GenericObjectActionMessage(objectGuid, code) = pkt
sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match {
case Some(vehicle: Vehicle)
if vehicle.OwnerName.contains(player.Name) =>
vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID))
case Some(tool: Tool) =>
if (code == 35 &&
(tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser"))
) {
//maelstrom primary fire mode discharge (no target)
//aphelion_laser discharge (no target)
sessionLogic.shooting.handleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID))
} else {
sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect {
case vehicle: Vehicle
if vehicle.OwnerName.contains(player.Name) =>
vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool))
}
}
case _ =>
log.info(s"${player.Name} - $pkt")
}
}
def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = {
val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt
sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match {
case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) =>
sessionLogic.shooting.FindContainedWeapon match {
case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) =>
vehicle.Actor ! SpecialEmp.Burst()
case _ => ()
}
case _ =>
log.info(s"${player.Name} - $pkt")
}
}
def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = {
val GenericObjectStateMsg(_, _) = pkt
log.info(s"${player.Name} - $pkt")
}
def handleGenericAction(pkt: GenericActionMessage): Unit = {
val GenericActionMessage(action) = pkt
if (player != null) {
val (toolOpt, definition) = player.Slot(0).Equipment match {
case Some(tool: Tool) =>
(Some(tool), tool.Definition)
case _ =>
(None, GlobalDefinitions.bullet_9mm)
}
action match {
case GenericAction.DropSpecialItem =>
ops.dropSpecialSlotItem()
case GenericAction.MaxAnchorsExtend_RCV =>
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.PlanetsideAttribute(player.GUID, 19, 1)
)
definition match {
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
val tool = toolOpt.get
tool.ToFireMode = 1
sendResponse(ChangeFireModeMessage(tool.GUID, 1))
case GlobalDefinitions.trhev_pounder =>
val tool = toolOpt.get
val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 }
else { 4 }
tool.ToFireMode = convertFireModeIndex
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
case _ =>
log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}")
}
case GenericAction.MaxAnchorsRelease_RCV =>
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.PlanetsideAttribute(player.GUID, 19, 0)
)
definition match {
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
val tool = toolOpt.get
tool.ToFireMode = 0
sendResponse(ChangeFireModeMessage(tool.GUID, 0))
case GlobalDefinitions.trhev_pounder =>
val tool = toolOpt.get
val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 }
tool.ToFireMode = convertFireModeIndex
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
case _ =>
log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}")
}
case GenericAction.MaxSpecialEffect_RCV =>
if (player.ExoSuit == ExoSuitType.MAX) {
ops.toggleMaxSpecialState(enable = true)
} else {
log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect")
}
case GenericAction.StopMaxSpecialEffect_RCV =>
if (player.ExoSuit == ExoSuitType.MAX) {
player.Faction match {
case PlanetSideEmpire.NC =>
ops.toggleMaxSpecialState(enable = false)
case _ =>
log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability")
}
} else {
log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect")
}
case GenericAction.AwayFromKeyboard_RCV =>
AvatarActor.savePlayerLocation(player)
player.AwayFromKeyboard = true
case GenericAction.BackInGame_RCV =>
player.AwayFromKeyboard = false
case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
if (!avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
}
case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF
if (avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
}
case _ =>
log.warn(s"GenericActionMessage: ${player.Name} can't handle $action")
}
}
}
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
player.BailProtection = false
val GenericCollisionMsg(ctype, p, _, _, pv, _, _, _, _, _, _, _) = pkt
if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
if (ops.heightTrend) {
ops.heightHistory = ops.heightLast
}
else {
ops.heightLast = ops.heightHistory
}
}
(ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match {
case (CollisionIs.OfInfantry, Some(user: Player))
if user == player => ()
case (CollisionIs.OfGroundVehicle, Some(v: Vehicle))
if v.Seats(0).occupant.contains(player) =>
v.BailProtection = false
case (CollisionIs.OfAircraft, Some(v: Vehicle))
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => ()
case (CollisionIs.BetweenThings, _) =>
log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
case _ => ()
}
}
def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = {
val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt
avatarActor ! AvatarActor.AddFirstTimeEvent(eventName)
}
def handleBugReport(pkt: PlanetSideGamePacket): Unit = {
val BugReportMessage(
_/*versionMajor*/,
_/*versionMinor*/,
_/*versionDate*/,
_/*bugType*/,
_/*repeatable*/,
_/*location*/,
_/*zone*/,
_/*pos*/,
_/*summary*/,
_/*desc*/
) = pkt
log.warn(s"${player.Name} filed a bug report - it might be something important")
log.debug(s"$pkt")
}
def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = {
val FacilityBenefitShieldChargeRequestMessage(_) = pkt
val vehicleGuid = player.VehicleSeated
continent
.GUID(vehicleGuid)
.foreach {
case obj: Vitality if obj.Destroyed => () //some entities will try to charge even if destroyed
case obj: Vehicle if obj.MountedIn.nonEmpty => () //cargo vehicles need to be excluded
case obj: Vehicle => ops.commonFacilityShieldCharging(obj)
case obj: TurretDeployable => ops.commonFacilityShieldCharging(obj)
case _ if vehicleGuid.nonEmpty => ()
case _ => ()
}
}
def handleBattleplan(pkt: BattleplanMessage): Unit = {
/* can not draw battleplan */
//todo csr exclusive battleplan channel
}
def handleBindPlayer(pkt: BindPlayerMessage): Unit = {
val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
}
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
val CreateShortcutMessage(_, slot, shortcutOpt) = pkt
shortcutOpt match {
case Some(shortcut) =>
avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
case None =>
avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
}
}
def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = {
val ChangeShortcutBankMessage(_, _) = pkt
}
def handleFriendRequest(pkt: FriendsRequest): Unit = {
val FriendsRequest(action, name) = pkt
avatarActor ! AvatarActor.MemberListRequest(action, name)
}
def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = {
val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt
(continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match {
case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle =>
if (alert == TerrainCondition.Unsafe) {
log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling")
}
case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) =>
if (alert == TerrainCondition.Unsafe) {
log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle")
}
case (Some(_: Vehicle), _) =>
log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain")
case (Some(packetThing), _) =>
log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain")
case _ =>
log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found")
}
}
def handleActionCancel(pkt: ActionCancelMessage): Unit = {
val ActionCancelMessage(_, _, _) = pkt
ops.progressBarUpdate.cancel()
ops.progressBarValue = None
}
def handleTrade(pkt: TradeMessage): Unit = {
val TradeMessage(trade) = pkt
log.trace(s"${player.Name} wants to trade for some reason - $trade")
}
def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { /* intentionally blank */ }
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
ops.handleObjectDetected(pkt)
}
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
ops.handleTargetingImplantRequest(pkt)
}
def handleHitHint(pkt: HitHint): Unit = {
val HitHint(_, _) = pkt
}
/* messages */
def handleRenewCharSavedTimer(): Unit = { /* */ }
def handleRenewCharSavedTimerMsg(): Unit = { /* */ }
def handleSetAvatar(avatar: Avatar): Unit = {
session = session.copy(avatar = avatar)
if (session.player != null) {
session.player.avatar = avatar
}
LivePlayerList.Update(avatar.id, avatar)
}
def handleReceiveAccountData(account: Account): Unit = { /* no need */ }
def handleUseCooldownRenew: BasicDefinition => Unit = {
case _: KitDefinition => ops.kitToBeUsed = None
case _ => ()
}
def handleAvatarResponse(avatar: Avatar): Unit = { /* no need */ }
def handleSetSpeed(speed: Float): Unit = {
session = session.copy(speed = speed)
}
def handleSetFlying(flying: Boolean): Unit = {
session = session.copy(flying = flying)
}
def handleSetSpectator(spectator: Boolean): Unit = {
session.player.spectator = spectator
}
def handleKick(player: Player, time: Option[Long]): Unit = {
ops.administrativeKick(player, time)
}
def handleSilenced(isSilenced: Boolean): Unit = { /* can not be silenced */ }
def handleItemPutInSlot(msg: Containable.ItemPutInSlot): Unit = {
log.debug(s"ItemPutInSlot: $msg")
}
def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit = {
log.debug(s"CanNotPutItemInSlot: $msg")
}
def handleReceiveDefaultMessage(default: Any, sender: ActorRef): Unit = {
log.warn(s"Invalid packet class received: $default from $sender")
}
/* supporting functions */
def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
if (!player.spectator) {
//opens for everyone
equipment match {
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
door.Actor ! CommonMessages.Use(player, Some(Float.MaxValue))
case _ =>
door.Actor ! CommonMessages.Use(player)
}
}
}
private def maxCapacitorTick(jumpThrust: Boolean): Unit = {
if (player.ExoSuit == ExoSuitType.MAX) {
val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0
player.CapacitorState match {
case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate)
case CapacitorStateType.Charging => maxCapacitorTickCharging(activate)
case _ => maxCapacitorTickIdle(activate)
}
} else if (player.CapacitorState != CapacitorStateType.Idle) {
player.CapacitorState = CapacitorStateType.Idle
}
}
private def maxCapacitorTickIdle(activate: Boolean): Unit = {
if (activate) {
player.CapacitorState = CapacitorStateType.Discharging
//maxCapacitorTickDischarging(activate)
} else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
player.CapacitorState = CapacitorStateType.Charging
}
}
private def maxCapacitorTickDischarging(activate: Boolean): Unit = {
if (activate) {
val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000
val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff
player.Capacitor -= drainAmount
sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
} else if (player.Capacitor == 0) {
if (player.Faction == PlanetSideEmpire.TR) {
ops.toggleMaxSpecialState(enable = false)
}
player.Capacitor = player.ExoSuitDef.MaxCapacitor.toFloat
sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
player.CapacitorState = CapacitorStateType.Idle
} else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
player.CapacitorState = CapacitorStateType.Charging
} else {
player.CapacitorState = CapacitorStateType.Idle
}
}
private def maxCapacitorTickCharging(activate: Boolean): Unit = {
val maxCapacitor = player.ExoSuitDef.MaxCapacitor
if (activate) {
player.CapacitorState = CapacitorStateType.Discharging
//maxCapacitorTickDischarging(activate)
} else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
player.Capacitor = maxCapacitor.toFloat
sendResponse(PlanetsideAttributeMessage(player.GUID, 7, maxCapacitor))
} else {
player.CapacitorState = CapacitorStateType.Idle
}
}
}

View file

@ -0,0 +1,334 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.ActorContext
import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.vehicles.AccessPermissionGroup
import net.psforever.objects.vital.InGameHistory
import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3}
object MountHandlerLogic {
def apply(ops: SessionMountHandlers): MountHandlerLogic = {
new MountHandlerLogic(ops, ops.context)
}
}
class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
/* packets */
def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
//can only mount vehicle when not in csr spectator mode
if (!player.spectator) {
ops.handleMountVehicle(pkt)
}
}
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
//can't do this if we're not in vehicle, so also not csr spectator
ops.handleDismountVehicle(pkt.copy(bailType = BailType.Bailed))
}
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
//can't do this if we're not in vehicle, so also not csr spectator
ops.handleMountVehicleCargo(pkt)
}
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
//can't do this if we're not in vehicle, so also not csr spectator
ops.handleDismountVehicleCargo(pkt.copy(bailed = true))
}
/* response handlers */
/**
* na
*
* @param tplayer na
* @param reply na
*/
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
reply match {
case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcess()
sessionLogic.terminals.CancelAllProximityUnits()
ops.MountingAction(tplayer, obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
sessionLogic.zoning.CancelZoningProcess()
sessionLogic.terminals.CancelAllProximityUnits()
ops.MountingAction(tplayer, obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.ant =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.quadstealth =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
//exclusive to the wraith, cloak state matches the cloak state of the driver
//phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
obj.Cloaked = tplayer.Cloaked
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if seatNumber == 0 =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition.MaxCapacitor > 0 =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcess()
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
sessionLogic.zoning.CancelZoningProcess()
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber)
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcess()
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber)
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, _, _) =>
sessionLogic.zoning.CancelZoningProcess()
log.warn(
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
)
case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcess()
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber)
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Mountable, _, _) =>
log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
ops.DismountAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
//dismount to hart lobby
val pguid = player.GUID
val sguid = obj.GUID
val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
tplayer.Position = pos
sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true))
continent.LocalEvents ! LocalServiceMessage(
continent.id,
LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang))
)
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
//get ready for orbital drop
val pguid = player.GUID
val events = continent.VehicleEvents
ops.DismountAction(tplayer, obj, seatNum)
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
events ! VehicleServiceMessage(
player.Name,
VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message
)
//when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky
//the player will fall to the ground and is perfectly vulnerable in this state
//additionally, our player must exist in the current zone
//having no in-game avatar target will throw us out of the map screen when deploying and cause softlock
events ! VehicleServiceMessage(
player.Name,
VehicleAction.SendResponse(
Service.defaultPlayerGUID,
PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay
)
)
events ! VehicleServiceMessage(
continent.id,
VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player
)
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if obj.Definition == GlobalDefinitions.droppod =>
sessionLogic.general.unaccessContainer(obj)
ops.DismountAction(tplayer, obj, seatNum)
obj.Actor ! Vehicle.Deconstruct()
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if tplayer.GUID == player.GUID &&
obj.isFlying &&
obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
// 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: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
//todo: kick cargo passengers out. To be added after PR #216 is merged
ops.DismountVehicleAction(tplayer, obj, seatNum)
obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if tplayer.GUID == player.GUID =>
ops.DismountVehicleAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
)
case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
ops.DismountAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Mountable, _, _) =>
log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
case Mountable.CanNotMount(obj: Vehicle, seatNumber) =>
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
obj.GetSeatFromMountPoint(seatNumber).collect {
case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
sendResponse(
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None)
)
}
case Mountable.CanNotMount(obj: Mountable, seatNumber) =>
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Normal)
if obj.DeploymentState == DriveState.AutoPilot =>
if (!player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotDismountAtThisTime"))
}
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.Definition == GlobalDefinitions.droppod =>
if (!player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@CannotBailFromDroppod"))
}
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.DeploymentState == DriveState.AutoPilot =>
if (!player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime"))
}
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.Health <= (obj.MaxHealth * .35).round && GlobalDefinitions.isFlightVehicle(obj.Definition) =>
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@BailingMechanismFailure_Pilot"))
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if {
continent
.blockMap
.sector(obj)
.buildingList
.exists {
case wg: WarpGate =>
Vector3.DistanceSquared(obj.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2)
case _ =>
false
}
} =>
if (!player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@Vehicle_CannotBailInWarpgateEnvelope"))
}
case Mountable.CanNotDismount(obj: Vehicle, _, _)
if obj.isMoving(test = 1f) =>
ops.handleDismountVehicle(DismountVehicleMsg(player.GUID, BailType.Bailed, wasKickedByDriver=true))
if (!player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@TooFastToDismount"))
}
case Mountable.CanNotDismount(obj, seatNum, _) =>
log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed")
}
}
/* support functions */
}

View file

@ -0,0 +1,92 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import net.psforever.actors.session.support.{AvatarHandlerFunctions, ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.objects.serverobject.ServerObject
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.{Player, Session, Vehicle}
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSidePacket
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.SpectatorChannel
import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage}
import net.psforever.types.{ChatMessageType, SquadRequestType}
//
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
import net.psforever.packet.game.ChatMsg
class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse)
val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse)
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
val vehicleResponse: VehicleHandlerFunctions = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations)
override def switchTo(session: Session): Unit = {
val player = session.player
val continent = session.zone
val pguid = player.GUID
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
//
data.squadService ! SquadServiceMessage(
player,
continent,
SquadAction.Membership(SquadRequestType.Leave, player.CharId, Some(player.CharId), player.Name, None)
)
if (requireDismount(data, continent, player)) {
CustomerServiceRepresentativeMode.renderPlayer(data, continent, player)
}
//
player.spectator = true
data.chat.JoinChannel(SpectatorChannel)
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid))
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE ON"))
}
override def switchFrom(session: Session): Unit = {
val player = data.player
val pguid = player.GUID
val continent = data.continent
val avatarId = player.Definition.ObjectId
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
//
player.spectator = false
data.chat.LeaveChannel(SpectatorChannel)
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.LoadPlayer(pguid, avatarId, pguid, player.Definition.Packet.ConstructorData(player).get, None)
)
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
}
private def requireDismount(data: SessionData, zone: Zone, player: Player): Boolean = {
data.vehicles.GetMountableAndSeat(None, player, zone) match {
case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 =>
data.vehicles.ServerVehicleOverrideStop(obj)
obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat
obj.Actor ! Mountable.TryDismount(player, seatNum)
player.VehicleSeated = None
true
case (Some(obj), Some(seatNum)) =>
obj.Actor ! Mountable.TryDismount(player, seatNum)
player.VehicleSeated = None
true
case _ =>
player.VehicleSeated = None
false
}
}
}
case object SpectateAsCustomerServiceRepresentativeMode extends PlayerMode {
def setup(data: SessionData): ModeLogic = {
new SpectatorCSRModeLogic(data)
}
}

View file

@ -0,0 +1,362 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
import net.psforever.objects.{Default, LivePlayerList}
import net.psforever.objects.avatar.Avatar
import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction}
import net.psforever.services.chat.SquadChannel
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype}
object SquadHandlerLogic {
def apply(ops: SessionSquadHandlers): SquadHandlerLogic = {
new SquadHandlerLogic(ops, ops.context)
}
}
class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
private val squadService: ActorRef = ops.squadService
/* packet */
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
/*if (!player.spectator) {
val SquadDefinitionActionMessage(u1, u2, action) = pkt
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
}*/
}
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
/* if (!player.spectator) {
val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
squadService ! SquadServiceMessage(
player,
continent,
SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
)
}*/
}
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {
if (!player.spectator) {
val SquadWaypointRequest(request, _, wtype, unk, info) = pkt
val time = System.currentTimeMillis()
val subtype = wtype.subtype
if (subtype == WaypointSubtype.Squad) {
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
} else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
//guarding against duplicating laze waypoints
waypointCooldown = time
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
}
}
}
/* response handlers */
def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
if (!excluded.exists(_ == avatar.id)) {
response match {
case SquadResponse.ListSquadFavorite(line, task) =>
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
case SquadResponse.InitList(infos) =>
sendResponse(ReplicationStreamMessage(infos))
case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
sendResponse(
ReplicationStreamMessage(
6,
None,
infos.map {
case (index, squadInfo) =>
SquadListing(index, squadInfo)
}.toVector
)
)
case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
sendResponse(
ReplicationStreamMessage(
1,
None,
infos.map { index =>
SquadListing(index, None)
}.toVector
)
)
case SquadResponse.SquadDecoration(guid, squad) =>
val decoration = if (
ops.squadUI.nonEmpty ||
squad.Size == squad.Capacity ||
{
val offer = avatar.certifications
!squad.Membership.exists { _.isAvailable(offer) }
}
) {
SquadListDecoration.NotAvailable
} else {
SquadListDecoration.Available
}
sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
case SquadResponse.Detail(guid, detail) =>
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
case SquadResponse.IdentifyAsSquadLeader(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
case SquadResponse.SetListSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) =>
val name = request_type match {
case SquadResponseType.Invite if unk5 =>
//the name of the player indicated by unk3 is needed
LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
case Some(player) =>
player.name
case None =>
player_name
}
case _ =>
player_name
}
sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6))
case SquadResponse.WantsSquadPosition(_, name) =>
sendResponse(
ChatMsg(
ChatMessageType.CMT_SQUAD,
wideContents=true,
name,
s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
None
)
)
case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
val avatarId = avatar.id
val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
.filter { case (mem, index) =>
mem.CharId > 0 && positionsToUpdate.contains(index)
}
membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
case Some((ourMember, ourIndex)) =>
//we are joining the squad
//load each member's entry (our own too)
ops.squad_supplement_id = squad.GUID.guid + 1
membershipPositions.foreach {
case (member, index) =>
sendResponse(
SquadMemberEvent.Add(
ops.squad_supplement_id,
member.CharId,
index,
member.Name,
member.ZoneId,
outfit_id = 0
)
)
ops.squadUI(member.CharId) =
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
}
//repeat our entry
sendResponse(
SquadMemberEvent.Add(
ops.squad_supplement_id,
ourMember.CharId,
ourIndex,
ourMember.Name,
ourMember.ZoneId,
outfit_id = 0
)
)
//turn lfs off
if (avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
}
val playerGuid = player.GUID
val factionChannel = s"${player.Faction}"
//squad colors
ops.GiveSquadColorsToMembers()
ops.GiveSquadColorsForOthers(playerGuid, factionChannel, ops.squad_supplement_id)
//associate with member position in squad
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
ops.updateSquadRef = ref
ops.updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad
sessionLogic.chat.JoinChannel(SquadChannel(squad.GUID))
case _ =>
//other player is joining our squad
//load each member's entry
ops.GiveSquadColorsToMembers(
membershipPositions.map {
case (member, index) =>
val charId = member.CharId
sendResponse(
SquadMemberEvent.Add(ops.squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
)
ops.squadUI(charId) =
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
charId
}
)
}
//send an initial dummy update for map icon(s)
sendResponse(
SquadState(
PlanetSideGUID(ops.squad_supplement_id),
membershipPositions.map { case (member, _) =>
SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
}
)
)
case SquadResponse.Leave(squad, positionsToUpdate) =>
positionsToUpdate.find({ case (member, _) => member == avatar.id }) match {
case Some((ourMember, ourIndex)) =>
//we are leaving the squad
//remove each member's entry (our own too)
ops.updateSquadRef = Default.Actor
positionsToUpdate.foreach {
case (member, index) =>
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
ops.squadUI.remove(member)
}
//uninitialize
val playerGuid = player.GUID
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
ops.GiveSquadColorsToSelf(value = 0)
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
avatarActor ! AvatarActor.SetLookingForSquad(false)
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
ops.squad_supplement_id = 0
ops.squadUpdateCounter = 0
ops.updateSquad = ops.NoSquadUpdates
sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID))
case _ =>
//remove each member's entry
ops.GiveSquadColorsToMembers(
positionsToUpdate.map {
case (member, index) =>
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
ops.squadUI.remove(member)
member
},
value = 0
)
}
case SquadResponse.AssignMember(squad, from_index, to_index) =>
//we've already swapped position internally; now we swap the cards
ops.SwapSquadUIElements(squad, from_index, to_index)
case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
if (promotedPlayer != player.CharId) {
//demoted from leader; no longer lfsm
if (player.avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
}
}
sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
//the players have already been swapped in the backend object
ops.PromoteSquadUIElements(squad, from_index)
case SquadResponse.UpdateMembers(_, positions) =>
val pairedEntries = positions.collect {
case entry if ops.squadUI.contains(entry.char_id) =>
(entry, ops.squadUI(entry.char_id))
}
//prune entries
val updatedEntries = pairedEntries
.collect({
case (entry, element) if entry.zone_number != element.zone =>
//zone gets updated for these entries
sendResponse(
SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number)
)
ops.squadUI(entry.char_id) =
SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
case (entry, element)
if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
//other elements that need to be updated
ops.squadUI(entry.char_id) =
SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
})
.filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
if (updatedEntries.nonEmpty) {
sendResponse(
SquadState(
PlanetSideGUID(ops.squad_supplement_id),
updatedEntries.map { entry =>
SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
}
)
)
}
case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
case SquadResponse.SquadSearchResults(_/*results*/) =>
//TODO positive squad search results message?
// if(results.nonEmpty) {
// results.foreach { guid =>
// sendResponse(SquadDefinitionActionMessage(
// guid,
// 0,
// SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
// )
// }
// } else {
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
// }
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
case SquadResponse.InitWaypoints(char_id, waypoints) =>
waypoints.foreach {
case (waypoint_type, info, unk) =>
sendResponse(
SquadWaypointEvent.Add(
ops.squad_supplement_id,
char_id,
waypoint_type,
WaypointEvent(info.zone_number, info.pos, unk)
)
)
}
case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
sendResponse(
SquadWaypointEvent.Add(
ops.squad_supplement_id,
char_id,
waypoint_type,
WaypointEvent(info.zone_number, info.pos, unk)
)
)
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type))
case _ => ()
}
}
}
}

View file

@ -0,0 +1,116 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions}
import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.guid.TaskWorkflow
import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal}
import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage}
object TerminalHandlerLogic {
def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
new TerminalHandlerLogic(ops, ops.context)
}
}
class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
if (player.spectator) {
val ItemTransactionMessage(terminal_guid, _, _, _, _, _) = pkt
sessionLogic.zoning.CancelZoningProcess()
continent
.GUID(terminal_guid)
.collect { case t: Terminal => t.Definition }
.collect { case t: OrderTerminalDefinition => t }
.map(t => t.Request(player, pkt))
.collect {
case order: Terminal.BuyVehicle =>
//do not handle transaction
order
}
.orElse {
ops.handleItemTransaction(pkt)
None
}
} else {
ops.handleItemTransaction(pkt)
}
}
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
ops.handleProximityTerminalUse(pkt)
}
def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
sessionLogic.zoning.CancelZoningProcess()
ops.handleFavoritesRequest(pkt)
}
/**
* na
* @param tplayer na
* @param msg na
* @param order na
*/
def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
order match {
case Terminal.BuyEquipment(item) =>
TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
continent.GUID(tplayer.VehicleSeated) match {
case Some(v: Vehicle) => v
case _ => player
},
tplayer,
msg.terminal_guid
)(item))
case Terminal.SellEquipment() =>
SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
case Terminal.LearnCertification(cert) =>
avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert)
ops.lastTerminalOrderFulfillment = true
case Terminal.SellCertification(cert) =>
avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
ops.lastTerminalOrderFulfillment = true
case Terminal.LearnImplant(implant) =>
avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
ops.lastTerminalOrderFulfillment = true
case Terminal.SellImplant(implant) =>
avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
ops.lastTerminalOrderFulfillment = true
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
ops.buyVehicle(msg.terminal_guid, msg.transaction_type, vehicle, weapons, trunk)
ops.lastTerminalOrderFulfillment = true
case Terminal.NoDeal() if msg != null =>
val transaction = msg.transaction_type
log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
ops.lastTerminalOrderFulfillment = true
case _ =>
val terminal = msg.terminal_guid.guid
continent.GUID(terminal) match {
case Some(term: Terminal) =>
log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order")
case Some(_) =>
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal")
case None =>
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal")
}
ops.lastTerminalOrderFulfillment = true
}
}
}

View file

@ -0,0 +1,397 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
object VehicleLogic {
def apply(ops: VehicleOperations): VehicleLogic = {
new VehicleLogic(ops, ops.context)
}
}
class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/* packets */
def handleVehicleState(pkt: VehicleStateMessage): Unit = {
val VehicleStateMessage(
vehicle_guid,
unk1,
pos,
ang,
vel,
is_flying,
unk6,
unk7,
wheels,
is_decelerating,
is_cloaked
) = pkt
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z)
if (obj.MountedIn.isEmpty) {
sessionLogic.updateBlockMap(obj, pos)
}
topOffHealthOfPlayer()
topOffHealth(obj)
player.Position = pos //convenient
if (obj.WeaponControlledFromSeat(0).isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = pos
obj.Orientation = ang
if (obj.MountedIn.isEmpty) {
if (obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel
} else {
obj.Velocity = Some(Vector3.Zero)
}
if (obj.Definition.CanFly) {
obj.Flying = is_flying //usually Some(7)
}
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
} else {
obj.Velocity = None
obj.Flying = None
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
player.GUID,
vehicle_guid,
unk1,
obj.Position,
ang,
obj.Velocity,
if (obj.isFlying) {
is_flying
} else {
None
},
unk6,
unk7,
wheels,
is_decelerating,
obj.Cloaked
)
)
sessionLogic.squad.updateSquad()
player.allowInteraction = false
obj.zoneInteractions()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
)
case _ => ()
}
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
val FrameVehicleStateMessage(
vehicle_guid,
unk1,
pos,
ang,
vel,
unk2,
unk3,
unk4,
is_crouched,
is_airborne,
ascending_flight,
flight_time,
unk9,
unkA
) = pkt
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
topOffHealthOfPlayer()
topOffHealth(obj)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) =>
sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ =>
(pos, ang, vel, true)
}
player.Position = position //convenient
if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = position
obj.Orientation = angle
obj.Velocity = velocity
// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
// //dev stuff goes here
// }
// else
// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
// //dev stuff goes here
// }
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
if (notMountedState) {
if (obj.DeploymentState != DriveState.Kneeling) {
if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight)
obj.Actor ! BfrFlight.Soaring(flight)
} else if (obj.Flying.nonEmpty) {
obj.Flying = None
obj.Actor ! BfrFlight.Landed
}
} else {
obj.Velocity = None
obj.Flying = None
}
player.allowInteraction = false
obj.zoneInteractions()
} else {
obj.Velocity = None
obj.Flying = None
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.FrameVehicleState(
player.GUID,
vehicle_guid,
unk1,
position,
angle,
velocity,
unk2,
unk3,
unk4,
is_crouched,
is_airborne,
ascending_flight,
flight_time,
unk9,
unkA
)
)
sessionLogic.squad.updateSquad()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
)
case _ => ()
}
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
//is COSM our primary upstream packet?
(o match {
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
case _ => (None, None)
}) match {
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
()
case (Some(obj: PlanetSideGameObject with Vitality), _) =>
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
topOffHealthOfPlayer()
topOffHealth(obj)
case _ =>
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
}
//the majority of the following check retrieves information to determine if we are in control of the child
tools.find { _.GUID == object_guid } match {
case None =>
//todo: old warning; this state is problematic, but can trigger in otherwise valid instances
//log.warn(
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
//)
case Some(_) =>
//TODO set tool orientation?
player.Orientation = Vector3(0f, pitch, yaw)
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
)
}
//TODO status condition of "playing getting out of vehicle to allow for late packets without warning
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState")
.collect {
case obj: Vehicle =>
import net.psforever.login.WorldSession.boolToInt
obj.Position = pos
obj.Orientation = ang
obj.Velocity = vel
sessionLogic.updateBlockMap(obj, pos)
obj.zoneInteractions()
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
player.GUID,
vehicle_guid,
unk1,
pos,
ang,
obj.Velocity,
obj.Flying,
0,
0,
15,
unk5 = false,
obj.Cloaked
)
)
}
}
def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
continent.GUID(vehicle_guid)
.collect {
case obj: Vehicle =>
continent.Transport ! Zone.Vehicle.TryDeploymentChange(obj, deploy_state)
obj
case obj =>
log.error(s"DeployRequest: ${player.Name} expected a vehicle, but found a ${obj.Definition.Name} instead")
obj
}
.orElse {
log.error(s"DeployRequest: ${player.Name} can not find entity $vehicle_guid")
avatarActor ! AvatarActor.SetVehicle(None) //todo is this safe
None
}
}
/* messages */
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
if (!Deployment.CheckForDeployState(state)) {
CanNotChangeDeployment(obj, state, "incorrect deploy state")
}
}
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
if (!Deployment.CheckForUndeployState(state)) {
CanNotChangeDeployment(obj, state, "incorrect undeploy state")
}
}
def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
CanNotChangeDeployment(obj, state, reason)
}
/* support functions */
/**
* Common reporting behavior when a `Deployment` object fails to properly transition between states.
* @param obj the game object that could not
* @param state the `DriveState` that could not be promoted
* @param reason a string explaining why the state can not or will not change
*/
private def CanNotChangeDeployment(
obj: PlanetSideServerObject with Deployment,
state: DriveState.Value,
reason: String
): Unit = {
if (obj.DeploymentState != DriveState.Mobile) {
obj.DeploymentState = DriveState.Mobile
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
)
}
}
private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
obj match {
case _: Player => topOffHealthOfPlayer()
case v: Vehicle => topOffHealthOfVehicle(v)
case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
case _ => ()
}
}
private def topOffHealthOfPlayer(): Unit = {
//driver below half health, full heal
val maxHealthOfPlayer = player.MaxHealth.toLong
if (player.Health < maxHealthOfPlayer * 0.5f) {
player.Health = maxHealthOfPlayer.toInt
player.LogActivity(player.ClearHistory().head)
sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
continent.AvatarEvents ! AvatarServiceMessage(sessionLogic.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
}
}
private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
topOffHealthOfPlayer()
topOffHealthOfGeneric(vehicle)
//vehicle shields below half, full shields
val maxShieldsOfVehicle = vehicle.MaxShields.toLong
val shieldsUi = vehicle.Definition.shieldUiAttribute
if (vehicle.Shields < maxShieldsOfVehicle) {
val guid = vehicle.GUID
vehicle.Shields = maxShieldsOfVehicle.toInt
sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
)
}
}
private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
topOffHealthOfPlayer()
//vehicle below half health, full heal
val guid = obj.GUID
val maxHealthOf = obj.MaxHealth.toLong
if (obj.Health < maxHealthOf) {
obj.Health = maxHealthOf.toInt
sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
)
}
}
}

View file

@ -0,0 +1,208 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.ActorContext
import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle}
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.zones.{Zone, ZoneProjectile}
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
import net.psforever.types.Vector3
object WeaponAndProjectileLogic {
def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
new WeaponAndProjectileLogic(ops, ops.context)
}
}
class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
def sessionLogic: SessionData = ops.sessionLogic
//private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/* packets */
def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
ops.handleWeaponFireOperations(pkt)
}
def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = {
val WeaponDelayFireMessage(_, _) = pkt
}
def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = {
ops.handleWeaponDryFire(pkt)
}
def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* laze is handled elsewhere */ }
def handleUplinkRequest(packet: UplinkRequest): Unit = { /* CUD not implemented yet */ }
def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* grenades are handled elsewhere */ }
def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = {
val ChangeFireStateMessage_Start(item_guid) = pkt
if (ops.shooting.isEmpty) {
sessionLogic.findEquipment(item_guid) match {
case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
ops.fireStateStartWhenPlayer(tool, item_guid)
case Some(tool: Tool) =>
ops.fireStateStartWhenMounted(tool, item_guid)
case Some(_) if player.VehicleSeated.isEmpty =>
ops.fireStateStartSetup(item_guid)
ops.fireStateStartPlayerMessages(item_guid)
case Some(_) =>
ops.fireStateStartSetup(item_guid)
ops.fireStateStartMountedMessages(item_guid)
case None =>
log.warn(s"ChangeFireState_Start: can not find $item_guid")
}
}
}
def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = {
val ChangeFireStateMessage_Stop(item_guid) = pkt
val now = System.currentTimeMillis()
ops.prefire -= item_guid
ops.shootingStop += item_guid -> now
ops.shooting -= item_guid
sessionLogic.findEquipment(item_guid) match {
case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
ops.fireStateStopWhenPlayer(tool, item_guid)
case Some(tool: Tool) =>
ops.fireStateStopWhenMounted(tool, item_guid)
case Some(trigger: BoomerTrigger) =>
ops.fireStateStopPlayerMessages(item_guid)
continent.GUID(trigger.Companion).collect {
case boomer: BoomerDeployable =>
boomer.Actor ! CommonMessages.Use(player, Some(trigger))
}
case Some(_) if player.VehicleSeated.isEmpty =>
ops.fireStateStopPlayerMessages(item_guid)
case Some(_) =>
ops.fireStateStopMountedMessages(item_guid)
case _ =>
log.warn(s"ChangeFireState_Stop: can not find $item_guid")
}
sessionLogic.general.progressBarUpdate.cancel()
sessionLogic.general.progressBarValue = None
}
def handleReload(pkt: ReloadMessage): Unit = {
val ReloadMessage(item_guid, _, unk1) = pkt
ops.FindContainedWeapon match {
case (Some(obj: Player), tools) =>
ops.handleReloadWhenPlayer(item_guid, obj, tools, unk1)
case (Some(obj: PlanetSideServerObject with Container), tools) =>
ops.handleReloadWhenMountable(item_guid, obj, tools, unk1)
case (_, _) =>
log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool")
}
}
def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = {
ops.handleChangeAmmo(pkt)
}
def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = {
ops.handleChangeFireMode(pkt)
}
def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
ops.handleProjectileState(pkt)
}
def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = {
val LongRangeProjectileInfoMessage(guid, _, _) = pkt
ops.FindContainedWeapon match {
case (Some(_: Vehicle), weapons)
if weapons.exists(_.GUID == guid) => () //now what?
case _ => ()
}
}
def handleDirectHit(pkt: HitMessage): Unit = {
val list = ops.composeDirectDamageInformation(pkt)
if (!player.spectator) {
list.foreach {
case (target, projectile, _, _) =>
ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, target.Position)
}
//...
if (list.isEmpty) {
ops.handleProxyDamage(pkt.projectile_guid, pkt.hit_info.map(_.hit_pos).getOrElse(Vector3.Zero))
}
}
}
def handleSplashHit(pkt: SplashHitMessage): Unit = {
val list = ops.composeSplashDamageInformation(pkt)
if (!player.spectator) {
if (list.nonEmpty) {
val projectile = list.head._2
val explosionPosition = projectile.Position
val projectileGuid = projectile.GUID
val profile = projectile.profile
val (resolution1, resolution2) = profile.Aggravated match {
case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
(DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
case _ =>
(DamageResolution.Splash, DamageResolution.Splash)
}
//...
val (direct, others) = list.partition { case (_, _, hitPos, targetPos) => hitPos == targetPos }
direct.foreach {
case (target, _, _, _) =>
ops.resolveProjectileInteraction(target, projectile, resolution1, target.Position)
}
others.foreach {
case (target, _, _, _) =>
ops.resolveProjectileInteraction(target, projectile, resolution2, target.Position)
}
//...
if (
profile.HasJammedEffectDuration ||
profile.JammerProjectile ||
profile.SympatheticExplosion
) {
//can also substitute 'profile' for 'SpecialEmp.emp'
Zone.serverSideDamage(
continent,
player,
SpecialEmp.emp,
SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosionPosition),
SpecialEmp.prepareDistanceCheck(player, explosionPosition, player.Faction),
SpecialEmp.findAllBoomers(profile.DamageRadius)
)
}
if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
continent.Projectile ! ZoneProjectile.Remove(projectileGuid)
}
}
//...
ops.handleProxyDamage(pkt.projectile_uid, pkt.projectile_pos)
}
}
def handleLashHit(pkt: LashMessage): Unit = {
val list = ops.composeLashDamageInformation(pkt)
if (!player.spectator) {
list.foreach {
case (target, projectile, _, targetPosition) =>
ops.resolveProjectileInteraction(target, projectile, DamageResolution.Lash, targetPosition)
}
}
}
def handleAIDamage(pkt: AIDamage): Unit = {
val list = ops.composeAIDamageInformation(pkt)
if (!player.spectator && ops.confirmAIDamageTarget(pkt, list.map(_._1))) {
list.foreach {
case (target, projectile, _, targetPosition) =>
ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, targetPosition)
}
}
}
}

View file

@ -3,8 +3,15 @@ package net.psforever.actors.session.normal
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.AvatarHandlerFunctions import net.psforever.actors.session.support.AvatarHandlerFunctions
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.inventory.Container
import net.psforever.objects.{Default, PlanetSideGameObject}
import net.psforever.objects.serverobject.containable.ContainableBehavior import net.psforever.objects.serverobject.containable.ContainableBehavior
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.ImplantType import net.psforever.types.ImplantType
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -21,7 +28,7 @@ import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.zones.Zoning import net.psforever.objects.zones.Zoning
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage} import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage} import net.psforever.services.avatar.AvatarResponse
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3} import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
@ -311,10 +318,14 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
//redraw //redraw
if (maxhand) { if (maxhand) {
sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong))
TaskWorkflow.execute(HoldNewEquipmentUp(player)( val maxArmDefinition = GlobalDefinitions.MAXArms(subtype, player.Faction)
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), TaskWorkflow.execute(HoldNewEquipmentUp(player)(Tool(maxArmDefinition), slot = 0))
0 player.avatar.purchaseCooldown(maxArmDefinition)
)) .collect(a => a)
.getOrElse {
avatarActor ! AvatarActor.UpdatePurchaseTime(maxArmDefinition)
None
}
} }
//draw free hand //draw free hand
player.FreeHand.Equipment.foreach { obj => player.FreeHand.Equipment.foreach { obj =>
@ -342,6 +353,8 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
) )
} }
DropLeftovers(player)(drop) DropLeftovers(player)(drop)
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants
case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) => case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) =>
sendResponse(ArmorChangedMessage(target, exosuit, subtype)) sendResponse(ArmorChangedMessage(target, exosuit, subtype))
@ -394,14 +407,19 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0))) drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
//redraw //redraw
if (maxhand) { if (maxhand) {
val maxArmWeapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong))
TaskWorkflow.execute(HoldNewEquipmentUp(player)( TaskWorkflow.execute(HoldNewEquipmentUp(player)(Tool(maxArmWeapon), slot = 0))
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), player.avatar.purchaseCooldown(maxArmWeapon)
slot = 0 if (!oldHolsters.exists { case (e, _) => e.Definition == maxArmWeapon } &&
)) player.avatar.purchaseCooldown(maxArmWeapon).isEmpty) {
avatarActor ! AvatarActor.UpdatePurchaseTime(maxArmWeapon) //switching for first time causes cooldown
}
} }
sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
DropLeftovers(player)(drops) DropLeftovers(player)(drops)
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants
case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) => case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
//redraw handled by callbacks //redraw handled by callbacks
@ -455,6 +473,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) => case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
ops.facilityCaptureRewards(buildingId, zoneNumber, cep) ops.facilityCaptureRewards(buildingId, zoneNumber, cep)
case AvatarResponse.ShareKillExperienceWithSquad(killer, exp) =>
ops.shareKillExperienceWithSquad(killer, exp)
case AvatarResponse.SendResponse(msg) => case AvatarResponse.SendResponse(msg) =>
sendResponse(msg) sendResponse(msg)
@ -466,9 +487,33 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
case AvatarResponse.Killed(mount) => case AvatarResponse.Killed(cause, mount) =>
//log and chat messages //log and chat messages
val cause = player.LastDamage.flatMap { damage => //destroy display
val zoneChannel = continent.id
val events = continent.AvatarEvents
val pentry = PlayerSource(player)
cause
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
.orElse {
player.LastDamage.collect {
case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis =>
attack
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
}.flatten
} match {
case Some(adversarial) =>
events ! AvatarServiceMessage(
zoneChannel,
AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement)
)
case _ =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
//events chat and log
val excuse = player.LastDamage.flatMap { damage =>
val interaction = damage.interaction val interaction = damage.interaction
val reason = interaction.cause val reason = interaction.cause
val adversarial = interaction.adversarial.map { _.attacker } val adversarial = interaction.adversarial.map { _.attacker }
@ -480,15 +525,17 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
} }
adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") } adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
}.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
log.info(s"${player.Name} has died, killed by $cause") log.info(s"${player.Name} has died, killed by $excuse")
if (sessionLogic.shooting.shotsWhileDead > 0) { if (sessionLogic.shooting.shotsWhileDead > 0) {
log.warn( log.warn(
s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server"
) )
sessionLogic.shooting.shotsWhileDead = 0 sessionLogic.shooting.shotsWhileDead = 0
} }
//TODO other methods of death?
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
continent.actor ! ZoneActor.RewardThisDeath(player)
//player state changes //player state changes
sessionLogic.zoning.spawn.avatarActive = false sessionLogic.zoning.spawn.avatarActive = false
@ -501,9 +548,18 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
sessionLogic.zoning.zoningStatus = Zoning.Status.None sessionLogic.zoning.zoningStatus = Zoning.Status.None
sessionLogic.zoning.spawn.deadState = DeadState.Dead sessionLogic.zoning.spawn.deadState = DeadState.Dead
continent.GUID(mount).collect { case obj: Vehicle => continent.GUID(mount).collect {
case obj: Vehicle =>
killedWhileMounted(obj, resolvedPlayerGuid)
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
sessionLogic.general.unaccessContainer(obj) sessionLogic.general.unaccessContainer(obj)
case obj: PlanetSideGameObject with Mountable with Container =>
killedWhileMounted(obj, resolvedPlayerGuid)
sessionLogic.general.unaccessContainer(obj)
case obj: PlanetSideGameObject with Mountable =>
killedWhileMounted(obj, resolvedPlayerGuid)
} }
sessionLogic.actionsToCancel() sessionLogic.actionsToCancel()
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
@ -511,7 +567,10 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
sessionLogic.zoning.spawn.shiftPosition = Some(player.Position) sessionLogic.zoning.spawn.shiftPosition = Some(player.Position)
//respawn //respawn
val respawnTimer = 300000 //milliseconds
sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, player.Position, player.Faction, unk5=true))
sessionLogic.zoning.spawn.reviveTimer.cancel() sessionLogic.zoning.spawn.reviveTimer.cancel()
sessionLogic.zoning.spawn.reviveTimer = Default.Cancellable
if (player.death_by == 0) { if (player.death_by == 0) {
sessionLogic.zoning.spawn.randomRespawn(300.seconds) sessionLogic.zoning.spawn.randomRespawn(300.seconds)
} else { } else {
@ -524,16 +583,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.Revive(revivalTargetGuid) case AvatarResponse.Revive(revivalTargetGuid)
if resolvedPlayerGuid == revivalTargetGuid => if resolvedPlayerGuid == revivalTargetGuid =>
log.info(s"No time for rest, ${player.Name}. Back on your feet!") log.info(s"No time for rest, ${player.Name}. Back on your feet!")
sessionLogic.zoning.spawn.reviveTimer.cancel() ops.revive(revivalTargetGuid)
sessionLogic.zoning.spawn.deadState = DeadState.Alive
player.Revive
val health = player.Health
sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
)
/* uncommon messages (utility, or once in a while) */ /* uncommon messages (utility, or once in a while) */
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
@ -623,4 +673,13 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case _ => () case _ => ()
} }
} }
def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = {
val events = continent.AvatarEvents
ops.killedWhileMounted(obj, playerGuid)
//make player invisible on client
events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(playerGuid, 29, 1))
//only the dead player should "see" their own body, so that the death camera has something to focus on
events ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(playerGuid, playerGuid))
}
} }

View file

@ -2,12 +2,14 @@
package net.psforever.actors.session.normal package net.psforever.actors.session.normal
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.actors.session.SessionActor
import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
import net.psforever.objects.Session import net.psforever.objects.Session
import net.psforever.objects.avatar.ModePermissions import net.psforever.packet.game.{ChatMsg, ServerType, SetChatFilterMessage}
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
import net.psforever.services.chat.DefaultChannel import net.psforever.services.chat.DefaultChannel
import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
import net.psforever.util.Config import net.psforever.util.Config
object ChatLogic { object ChatLogic {
@ -19,34 +21,27 @@ object ChatLogic {
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
ops.CurrentSpectatorMode = SpectatorMode
ops.transitoryCommandEntered = None
def handleChatMsg(message: ChatMsg): Unit = { def handleChatMsg(message: ChatMsg): Unit = {
import net.psforever.types.ChatMessageType._ import net.psforever.types.ChatMessageType._
val isAlive = if (player != null) player.isAlive else false lazy val isAlive = avatar != null && player != null && player.isAlive
val perms = if (avatar != null) avatar.permissions else ModePermissions()
val gmCommandAllowed = (session.account.gm && perms.canGM) ||
Config.app.development.unprivilegedGmCommands.contains(message.messageType)
(message.messageType, message.recipient.trim, message.contents.trim) match { (message.messageType, message.recipient.trim, message.contents.trim) match {
/** Messages starting with ! are custom chat commands */ /** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") && case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session) => () customCommandMessages(message, session) => ()
case (CMT_FLY, recipient, contents) if gmCommandAllowed => case (CMT_ANONYMOUS, _, _) => ()
ops.commandFly(contents, recipient)
case (CMT_ANONYMOUS, _, _) => case (CMT_TOGGLE_GM, _, contents) if isAlive =>
// ? customCommandModerator(contents)
case (CMT_TOGGLE_GM, _, _) =>
// ?
case (CMT_CULLWATERMARK, _, contents) => case (CMT_CULLWATERMARK, _, contents) =>
ops.commandWatermark(contents) ops.commandWatermark(contents)
case (CMT_SPEED, _, contents) if gmCommandAllowed => case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive =>
ops.commandSpeed(message, contents) commandToggleSpectatorMode(contents)
case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) =>
ops.commandToggleSpectatorMode(session, contents)
case (CMT_RECALL, _, _) => case (CMT_RECALL, _, _) =>
ops.commandRecall(session) ops.commandRecall(session)
@ -63,28 +58,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_DESTROY, _, contents) if contents.matches("\\d+") => case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
ops.commandDestroy(session, message, contents) ops.commandDestroy(session, message, contents)
case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
ops.commandSetBaseResources(session, contents)
case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
ops.commandZoneLock(contents)
case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed =>
ops.commandZoneRotate()
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
ops.commandCaptureBase(session, message, contents)
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
if gmCommandAllowed =>
ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_GMTELL, _, _) if gmCommandAllowed =>
ops.commandSend(session, message, DefaultChannel)
case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_OPEN, _, _) if !player.silenced => case (CMT_OPEN, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
@ -100,54 +73,21 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_PLATOON, _, _) if !player.silenced => case (CMT_PLATOON, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_COMMAND, _, _) if gmCommandAllowed =>
ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_NOTE, _, _) => case (CMT_NOTE, _, _) =>
ops.commandSend(session, message, DefaultChannel) ops.commandSend(session, message, DefaultChannel)
case (CMT_SILENCE, _, _) if gmCommandAllowed =>
ops.commandSend(session, message, DefaultChannel)
case (CMT_SQUAD, _, _) => case (CMT_SQUAD, _, _) =>
ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
ops.commandWho(session) ops.commandWho(session)
case (CMT_ZONE, _, contents) if gmCommandAllowed =>
ops.commandZone(message, contents)
case (CMT_WARP, _, contents) if gmCommandAllowed =>
ops.commandWarp(session, message, contents)
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
ops.commandSetBattleRank(session, message, contents)
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
ops.commandSetCommandRank(session, message, contents)
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
ops.commandAddBattleExperience(message, contents)
case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed =>
ops.commandAddCommandExperience(message, contents)
case (CMT_TOGGLE_HAT, _, contents) => case (CMT_TOGGLE_HAT, _, contents) =>
ops.commandToggleHat(session, message, contents) ops.commandToggleHat(session, message, contents)
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) => case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
ops.commandToggleCosmetics(session, message, contents) ops.commandToggleCosmetics(session, message, contents)
case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed =>
ops.commandAddCertification(session, message, contents)
case (CMT_KICK, _, contents) if gmCommandAllowed =>
ops.commandKick(session, message, contents)
case (CMT_REPORTUSER, _, contents) =>
ops.commandReportUser(session, message, contents)
case _ => case _ =>
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission"))
} }
@ -192,21 +132,24 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case a :: b => (a, b) case a :: b => (a, b)
case _ => ("", Seq("")) case _ => ("", Seq(""))
} }
val perms = if (avatar != null) avatar.permissions else ModePermissions()
val gmBangCommandAllowed = (session.account.gm && perms.canGM) ||
Config.app.development.unprivilegedGmBangCommands.contains(command)
//try gm commands
val tryGmCommandResult = if (gmBangCommandAllowed) {
command match { command match {
case "whitetext" => Some(ops.customCommandWhitetext(session, params)) case "loc" => ops.customCommandLoc(session, message)
case "list" => Some(ops.customCommandList(session, params, message)) case "suicide" => ops.customCommandSuicide(session)
case "ntu" => Some(ops.customCommandNtu(session, params)) case "grenade" => ops.customCommandGrenade(session, log)
case "zonerotate" => Some(ops.customCommandZonerotate(params)) case "macro" => ops.customCommandMacro(session, params)
case "nearby" => Some(ops.customCommandNearby(session)) case "progress" => ops.customCommandProgress(session, params)
case _ => None case _ =>
} // command was not handled
} else { sendResponse(
None ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
true
} }
//try commands for all players if not caught as a gm command //try commands for all players if not caught as a gm command
val result = tryGmCommandResult match { val result = tryGmCommandResult match {
@ -223,21 +166,57 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case Some(out) => case Some(out) =>
out out
} }
if (!result) {
// command was not handled
sendResponse(
ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
}
result
} else { } else {
false // not a handled command false
}
}
def commandToggleSpectatorMode(contents: String): Boolean = {
ops.transitoryCommandEntered
.collect {
case CMT_TOGGLESPECTATORMODE => true
case CMT_TOGGLE_GM => false
}
.getOrElse {
val currentSpectatorActivation =
avatar.permissions.canSpectate ||
avatar.permissions.canGM ||
Config.app.world.serverType == ServerType.Development
contents.toLowerCase() match {
case "on" | "o" | "" if currentSpectatorActivation && !player.spectator =>
ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE)
context.self ! SessionActor.SetMode(ops.CurrentSpectatorMode)
true
case _ =>
false
}
}
}
def customCommandModerator(contents: String): Boolean = {
if (sessionLogic.zoning.maintainInitialGmState) {
sessionLogic.zoning.maintainInitialGmState = false
true
} else {
ops.transitoryCommandEntered
.collect {
case CMT_TOGGLE_GM => true
case CMT_TOGGLESPECTATORMODE => false
}
.getOrElse {
val currentCsrActivation =
avatar.permissions.canGM ||
Config.app.world.serverType == ServerType.Development
contents.toLowerCase() match {
case "on" | "o" | "" if currentCsrActivation =>
import net.psforever.actors.session.csr.CustomerServiceRepresentativeMode
ops.transitoryCommandEntered = Some(CMT_TOGGLE_GM)
context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode)
true
case _ =>
false
}
}
} }
} }
} }

View file

@ -5,49 +5,44 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, typed} import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory} import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike} import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.entity.WorldEntity import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
import net.psforever.objects.inventory.Container import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Building, WarpGate} import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech 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 import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, UtilityType, VehicleLockState} import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity, Vitality}
import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason}
import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ObjectClass import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.local.support.CaptureFlagManager import net.psforever.services.local.support.CaptureFlagManager
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -206,38 +201,36 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleEmote(pkt: EmoteMsg): Unit = { def handleEmote(pkt: EmoteMsg): Unit = {
val EmoteMsg(avatarGuid, emote) = pkt val EmoteMsg(avatarGuid, emote) = pkt
val pZone = player.Zone
sendResponse(EmoteMsg(avatarGuid, emote)) sendResponse(EmoteMsg(avatarGuid, emote))
pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote)))
}
pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote)))
}
} }
def handleDropItem(pkt: DropItemMessage): Unit = { def handleDropItem(pkt: DropItemMessage): Unit = {
val DropItemMessage(itemGuid) = pkt ops.handleDropItem(pkt) match {
(sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { case GeneralOperations.ItemDropState.Dropped =>
case (Some(anItem: Equipment), Some(heldItem))
if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
RemoveOldEquipmentFromInventory(player)(heldItem) case GeneralOperations.ItemDropState.NotDropped
case (Some(anItem: Equipment), Some(heldItem))
if anItem eq heldItem =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
DropEquipmentFromInventory(player)(heldItem)
case (Some(anItem: Equipment), _)
if continent.GUID(player.VehicleSeated).isEmpty => if continent.GUID(player.VehicleSeated).isEmpty =>
//suppress the warning message if in a vehicle log.warn(s"DropItem: ${player.Name} wanted to drop an item, but it wasn't at hand")
log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand") case GeneralOperations.ItemDropState.NotDropped =>
case (Some(obj), _) => log.warn(s"DropItem: ${player.Name} wanted to drop an item, but it was not equipment")
log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment")
case _ => () case _ => ()
} }
} }
def handlePickupItem(pkt: PickupItemMessage): Unit = { def handlePickupItem(pkt: PickupItemMessage): Unit = {
val PickupItemMessage(itemGuid, _, _, _) = pkt ops.handlePickupItem(pkt) match {
sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect { case GeneralOperations.ItemPickupState.PickedUp =>
case item: Equipment if player.Fit(item).nonEmpty =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
PickUpEquipmentFromGround(player)(item) case GeneralOperations.ItemPickupState.Dropped =>
case _: Equipment =>
sendResponse(ActionResultMessage.Fail(16)) //error code? sendResponse(ActionResultMessage.Fail(16)) //error code?
case _ => ()
} }
} }
@ -253,33 +246,17 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
def handleZipLine(pkt: ZipLineMessage): Unit = { def handleZipLine(pkt: ZipLineMessage): Unit = {
val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt ops.handleZipLine(pkt) match {
continent.zipLinePaths.find(x => x.PathId == pathId) match { case GeneralOperations.ZiplineBehavior.Teleporter =>
case Some(path) if path.IsTeleporter =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
val endPoint = path.ZipLinePoints.last case GeneralOperations.ZiplineBehavior.Zipline =>
sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
//todo: send to zone to show teleport animation to all clients
sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
case Some(_) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion")
action match { case GeneralOperations.ZiplineBehavior.Unsupported =>
case 0 =>
//travel along the zipline in the direction specified
sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
case 1 =>
//disembark from zipline at destination
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
case 2 =>
//get off by force
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
case _ =>
log.warn( log.warn(
s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" s"${player.Name} tried to do something with a zipline but can't handle it. action: ${pkt.action}, pathId: ${pkt.path_id}, zone: ${continent.id}"
) )
} case GeneralOperations.ZiplineBehavior.NotFound =>
case _ => log.warn(s"${player.Name} couldn't find a zipline path ${pkt.path_id} in zone ${continent.id}")
log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
} }
} }
@ -288,13 +265,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
//make sure this is the correct response for all cases //make sure this is the correct response for all cases
sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match {
case Some(vehicle: Vehicle) => case Some(vehicle: Vehicle) =>
/* line 1a: player is admin (and overrules other access requirements) */ /* line 1a: vehicle and player (as the owner) acknowledge each other */
/* line 1b: vehicle and player (as the owner) acknowledge each other */ /* line 1b: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */
/* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */
/* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
if ( if (
(session.account.gm || ((avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
(player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
(player.Faction == vehicle.Faction && (player.Faction == vehicle.Faction &&
(vehicle.Definition.CanBeOwned.nonEmpty && (vehicle.Definition.CanBeOwned.nonEmpty &&
(vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) && (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
@ -313,7 +288,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
continent.Projectile ! ZoneProjectile.Remove(objectGuid) continent.Projectile ! ZoneProjectile.Remove(objectGuid)
case Some(obj: BoomerTrigger) => case Some(obj: BoomerTrigger) =>
if (findEquipmentToDelete(objectGuid, obj)) { if (ops.findEquipmentToDelete(objectGuid, obj)) {
continent.GUID(obj.Companion) match { continent.GUID(obj.Companion) match {
case Some(boomer: BoomerDeployable) => case Some(boomer: BoomerDeployable) =>
boomer.Trigger = None boomer.Trigger = None
@ -325,14 +300,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
case Some(obj: Deployable) => case Some(obj: Deployable) =>
if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) { if (obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
obj.Actor ! Deployable.Deconstruct() obj.Actor ! Deployable.Deconstruct()
} else { } else {
log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it") log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
} }
case Some(obj: Equipment) => case Some(obj: Equipment) =>
findEquipmentToDelete(objectGuid, obj) ops.findEquipmentToDelete(objectGuid, obj)
case Some(thing) => case Some(thing) =>
log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}")
@ -342,99 +317,20 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
def handleMoveItem(pkt: MoveItemMessage): Unit = { def handleMoveItem(pkt: MoveItemMessage): Unit = {
val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt ops.handleMoveItem(pkt)
(
continent.GUID(sourceGuid),
continent.GUID(destinationGuid),
sessionLogic.validObject(itemGuid, decorator = "MoveItem")
) match {
case (
Some(source: PlanetSideServerObject with Container),
Some(destination: PlanetSideServerObject with Container),
Some(item: Equipment)
) =>
ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest))
case (None, _, _) =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object"
)
case (_, None, _) =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object"
)
case (_, _, None) => ()
case _ =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered"
)
}
} }
def handleLootItem(pkt: LootItemMessage): Unit = { def handleLootItem(pkt: LootItemMessage): Unit = {
val LootItemMessage(itemGuid, targetGuid) = pkt ops.handleLootItem(pkt)
(sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match {
case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) =>
//figure out the source
(
{
val findFunc: PlanetSideServerObject with Container => Option[
(PlanetSideServerObject with Container, Option[Int])
] = ops.findInLocalContainer(itemGuid)
findFunc(player.avatar.locker)
.orElse(findFunc(player))
.orElse(ops.accessedContainer match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
})
},
destination.Fit(item)
) match {
case (Some((source, Some(_))), Some(dest)) =>
ContainableMoveItem(player.Name, source, destination, item, dest)
case (None, _) =>
log.error(s"LootItem: ${player.Name} can not find where $item is put currently")
case (_, None) =>
log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination")
case _ =>
log.error(
s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered"
)
}
case (Some(obj), _) =>
log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}")
case (None, _) => ()
case (_, None) =>
log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid")
}
} }
def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
val AvatarImplantMessage(_, action, slot, status) = pkt ops.handleAvatarImplant(pkt) match {
if (action == ImplantAction.Activation) { case GeneralOperations.ImplantActivationBehavior.Activate | GeneralOperations.ImplantActivationBehavior.Deactivate =>
if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
//do not activate; play deactivation sound instead
sessionLogic.zoning.spawn.stopDeconstructing()
avatar.implants(slot).collect {
case implant if implant.active =>
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
case implant =>
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
}
} else {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant")
avatar.implants(slot) match { case GeneralOperations.ImplantActivationBehavior.NotFound =>
case Some(implant) => log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in ${pkt.implantSlot}")
if (status == 1) { case _ => ()
avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType)
} else {
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
}
case _ =>
log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot")
}
}
} }
} }
@ -449,47 +345,47 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
case Some(door: Door) => case Some(door: Door) =>
handleUseDoor(door, equipment) ops.handleUseDoor(door, equipment)
case Some(resourceSilo: ResourceSilo) => case Some(resourceSilo: ResourceSilo) =>
handleUseResourceSilo(resourceSilo, equipment) ops.handleUseResourceSilo(resourceSilo, equipment)
case Some(panel: IFFLock) => case Some(panel: IFFLock) =>
handleUseGeneralEntity(panel, equipment) ops.handleUseGeneralEntity(panel, equipment)
case Some(obj: Player) => case Some(obj: Player) =>
handleUsePlayer(obj, equipment, pkt) ops.handleUsePlayer(obj, equipment, pkt)
case Some(locker: Locker) => case Some(locker: Locker) =>
handleUseLocker(locker, equipment, pkt) ops.handleUseLocker(locker, equipment, pkt)
case Some(gen: Generator) => case Some(gen: Generator) =>
handleUseGeneralEntity(gen, equipment) ops.handleUseGeneralEntity(gen, equipment)
case Some(mech: ImplantTerminalMech) => case Some(mech: ImplantTerminalMech) =>
handleUseGeneralEntity(mech, equipment) ops.handleUseGeneralEntity(mech, equipment)
case Some(captureTerminal: CaptureTerminal) => case Some(captureTerminal: CaptureTerminal) =>
handleUseCaptureTerminal(captureTerminal, equipment) ops.handleUseCaptureTerminal(captureTerminal, equipment)
case Some(obj: FacilityTurret) => case Some(obj: FacilityTurret) =>
handleUseFacilityTurret(obj, equipment, pkt) ops.handleUseFacilityTurret(obj, equipment, pkt)
case Some(obj: Vehicle) => case Some(obj: Vehicle) =>
handleUseVehicle(obj, equipment, pkt) ops.handleUseVehicle(obj, equipment, pkt)
case Some(terminal: Terminal) => case Some(terminal: Terminal) =>
handleUseTerminal(terminal, equipment, pkt) ops.handleUseTerminal(terminal, equipment, pkt)
case Some(obj: SpawnTube) => case Some(obj: SpawnTube) =>
handleUseSpawnTube(obj, equipment) ops.handleUseSpawnTube(obj, equipment)
case Some(obj: SensorDeployable) => case Some(obj: SensorDeployable) =>
handleUseGeneralEntity(obj, equipment) ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TurretDeployable) => case Some(obj: TurretDeployable) =>
handleUseGeneralEntity(obj, equipment) ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TrapDeployable) => case Some(obj: TrapDeployable) =>
handleUseGeneralEntity(obj, equipment) ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: ShieldGeneratorDeployable) => case Some(obj: ShieldGeneratorDeployable) =>
handleUseGeneralEntity(obj, equipment) ops.handleUseGeneralEntity(obj, equipment)
case Some(obj: TelepadDeployable) => case Some(obj: TelepadDeployable) =>
handleUseTelepadDeployable(obj, equipment, pkt) ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem)
case Some(obj: Utility.InternalTelepad) => case Some(obj: Utility.InternalTelepad) =>
handleUseInternalTelepad(obj, pkt) ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem)
case Some(obj: CaptureFlag) => case Some(obj: CaptureFlag) =>
handleUseCaptureFlag(obj) ops.handleUseCaptureFlag(obj)
case Some(_: WarpGate) => case Some(_: WarpGate) =>
handleUseWarpGate(equipment) ops.handleUseWarpGate(equipment)
case Some(obj) => case Some(obj) =>
handleUseDefaultEntity(obj, equipment) ops.handleUseDefaultEntity(obj, equipment)
case None => () case None => ()
} }
} }
@ -520,19 +416,13 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
log.info(s"${player.Name} is constructing a $ammoType deployable") log.info(s"${player.Name} is constructing a $ammoType deployable")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
val dObj: Deployable = Deployables.Make(ammoType)() if (ammoType == DeployedItem.spitfire_turret || ammoType == DeployedItem.spitfire_cloaked ||
dObj.Position = pos ammoType == DeployedItem.spitfire_aa) {
dObj.Orientation = orient ops.handleDeployObject(continent, ammoType, pos, orient, OutsideOf, player.Faction, player, obj)
dObj.WhichSide = player.WhichSide }
dObj.Faction = player.Faction else {
dObj.AssignOwnership(player) ops.handleDeployObject(continent, ammoType, pos, orient, player.WhichSide, player.Faction, player, obj)
val tasking: TaskBundle = dObj match {
case turret: TurretDeployable =>
GUIDTask.registerDeployableTurret(continent.GUID, turret)
case _ =>
GUIDTask.registerObject(continent.GUID, dObj)
} }
TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj), context.self))
case Some(obj) => case Some(obj) =>
log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!")
case None => case None =>
@ -569,7 +459,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
) { ) {
//maelstrom primary fire mode discharge (no target) //maelstrom primary fire mode discharge (no target)
//aphelion_laser discharge (no target) //aphelion_laser discharge (no target)
sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) sessionLogic.shooting.handleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID))
} else { } else {
sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect { sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect {
case vehicle: Vehicle case vehicle: Vehicle
@ -749,7 +639,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
val curr = System.currentTimeMillis() val curr = System.currentTimeMillis()
(target1, t, target2) match { (target1, t, target2) match {
case (None, _, _) => () case (None, _, _) =>
()
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
if (updateCollisionHistoryForTarget(us, curr)) { if (updateCollisionHistoryForTarget(us, curr)) {
@ -821,10 +712,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
.foreach { .foreach {
case obj: Vitality if obj.Destroyed => () //some entities will try to charge even if destroyed case obj: Vitality if obj.Destroyed => () //some entities will try to charge even if destroyed
case obj: Vehicle if obj.MountedIn.nonEmpty => () //cargo vehicles need to be excluded case obj: Vehicle if obj.MountedIn.nonEmpty => () //cargo vehicles need to be excluded
case obj: Vehicle => case obj: Vehicle => ops.commonFacilityShieldCharging(obj)
commonFacilityShieldCharging(obj) case obj: TurretDeployable => ops.commonFacilityShieldCharging(obj)
case obj: TurretDeployable =>
commonFacilityShieldCharging(obj)
case _ if vehicleGuid.nonEmpty => case _ if vehicleGuid.nonEmpty =>
log.warn( log.warn(
s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find chargeable entity ${vehicleGuid.get.guid} in ${continent.id}" s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find chargeable entity ${vehicleGuid.get.guid} in ${continent.id}"
@ -846,13 +735,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
val CreateShortcutMessage(_, slot, shortcutOpt) = pkt ops.handleCreateShortcut(pkt)
shortcutOpt match {
case Some(shortcut) =>
avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
case None =>
avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
}
} }
def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = {
@ -902,39 +785,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
val ObjectDetectedMessage(_, _, _, targets) = pkt ops.handleObjectDetected(pkt)
sessionLogic.shooting.FindWeapon.foreach {
case weapon if weapon.Projectile.AutoLock =>
//projectile with auto-lock instigates a warning on the target
val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets)
val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0)
detectedTargets.foreach { target =>
continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode))
}
case _ => ()
}
} }
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
val TargetingImplantRequest(list) = pkt ops.handleTargetingImplantRequest(pkt)
val targetInfo: List[TargetInfo] = list.flatMap { x =>
continent.GUID(x.target_guid) match {
case Some(player: Player) =>
val health = player.Health.toFloat / player.MaxHealth
val armor = if (player.MaxArmor > 0) {
player.Armor.toFloat / player.MaxArmor
} else {
0
}
Some(TargetInfo(player.GUID, health, armor))
case _ =>
log.warn(
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
)
None
}
}
sendResponse(TargetingInfoMessage(targetInfo))
} }
def handleHitHint(pkt: HitHint): Unit = { def handleHitHint(pkt: HitHint): Unit = {
@ -989,13 +844,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
session = session.copy(flying = flying) session = session.copy(flying = flying)
} }
def handleSetSpectator(spectator: Boolean): Unit = { def handleSetSpectator(spectator: Boolean): Unit = { /* normal players can not flag spectate */ }
session.player.spectator = spectator
}
def handleKick(player: Player, time: Option[Long]): Unit = { def handleKick(player: Player, time: Option[Long]): Unit = {
ops.administrativeKick(player) ops.administrativeKick(player, time)
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
} }
def handleSilenced(isSilenced: Boolean): Unit = { def handleSilenced(isSilenced: Boolean): Unit = {
@ -1016,413 +868,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
/* supporting functions */ /* supporting functions */
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)
}
}
private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
val vehicleOpt = continent.GUID(player.avatar.vehicle)
(vehicleOpt, equipment) match {
case (Some(vehicle: Vehicle), Some(item))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle))
case (Some(vehicle: Vehicle), _)
if vehicle.Definition == GlobalDefinitions.ant &&
vehicle.DeploymentState == DriveState.Deployed &&
Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) =>
resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle))
case _ => ()
}
}
private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
if (obj.isBackpack) {
if (equipment.isEmpty) {
log.info(s"${player.Name} is looting the corpse of ${obj.Name}")
sendResponse(msg)
ops.accessContainer(obj)
}
} else if (!msg.unk3 && player.isAlive) { //potential kit use
(continent.GUID(msg.item_used_guid), ops.kitToBeUsed) match {
case (Some(kit: Kit), None) =>
ops.kitToBeUsed = Some(msg.item_used_guid)
player.Actor ! CommonMessages.Use(player, Some(kit))
case (Some(_: Kit), Some(_)) | (None, Some(_)) =>
//a kit is already queued to be used; ignore this request
sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None))
case (Some(item), _) =>
log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead")
case (None, None) =>
log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") }
} else if (msg.object_id == ObjectClass.avatar && msg.unk3) {
equipment match {
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank =>
obj.Actor ! CommonMessages.Use(player, equipment)
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
obj.Actor ! CommonMessages.Use(player, equipment)
case _ => ()
}
}
}
private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(locker, item)
case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty =>
log.info(s"${player.Name} is accessing a locker")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
val playerLocker = player.avatar.locker
sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456))
ops.accessContainer(playerLocker)
case _ => ()
}
}
private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(captureTerminal, item)
case _ if ops.specialItemSlotGuid.nonEmpty =>
continent.GUID(ops.specialItemSlotGuid) match {
case Some(llu: CaptureFlag) =>
if (llu.Target.GUID == captureTerminal.Owner.GUID) {
continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu))
} else {
log.info(
s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}"
)
}
case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU")
}
case _ => ()
}
}
private def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment.foreach { item =>
sendUseGeneralEntityMessage(obj, item)
obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path
}
}
private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(obj, item)
case None if player.Faction == obj.Faction =>
//access to trunk
if (
obj.AccessingTrunk.isEmpty &&
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
.contains(player.GUID))
) {
log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.AccessingTrunk = player.GUID
ops.accessContainer(obj)
sendResponse(msg)
}
case _ => ()
}
}
private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(terminal, item)
case None
if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction ||
terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL =>
val tdef = terminal.Definition
if (tdef.isInstanceOf[MatrixTerminalDefinition]) {
//TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sendResponse(
BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position)
)
} else if (
tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal ||
tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal
) {
findLocalVehicle match {
case Some(vehicle) =>
log.info(
s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}"
)
sendResponse(msg)
sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId))
case None =>
log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none")
}
} else if (tdef == GlobalDefinitions.teleportpad_terminal) {
//explicit request
log.info(s"${player.Name} is purchasing a router telepad")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
)
} else if (tdef == GlobalDefinitions.targeting_laser_dispenser) {
//explicit request
log.info(s"${player.Name} is purchasing a targeting laser")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0))
)
} else {
log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sendResponse(msg)
}
case _ => ()
}
}
private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(obj, item)
case None if player.Faction == obj.Faction =>
//deconstruction
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sessionLogic.actionsToCancel()
sessionLogic.terminals.CancelAllProximityUnits()
sessionLogic.zoning.spawn.startDeconstructing(obj)
case _ => ()
}
}
private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
if (equipment.isEmpty) {
(continent.GUID(obj.Router) match {
case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
case Some(vehicle) => Some(vehicle, None)
case None => None
}) match {
case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
player.WhichSide = vehicle.WhichSide
useRouterTelepadSystem(
router = vehicle,
internalTelepad = util,
remoteTelepad = obj,
src = obj,
dest = util
)
case Some((vehicle: Vehicle, None)) =>
log.error(
s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
)
case Some((o, _)) =>
log.error(
s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}"
)
obj.Actor ! Deployable.Deconstruct()
case _ => ()
}
}
}
private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
continent.GUID(obj.Telepad) match {
case Some(pad: TelepadDeployable) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
player.WhichSide = pad.WhichSide
useRouterTelepadSystem(
router = obj.Owner.asInstanceOf[Vehicle],
internalTelepad = obj,
remoteTelepad = pad,
src = obj,
dest = pad
)
case Some(o) =>
log.error(
s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}"
)
case None => ()
}
}
private def handleUseCaptureFlag(obj: CaptureFlag): Unit = {
// LLU can normally only be picked up the faction that owns it
ops.specialItemSlotGuid match {
case None if obj.Faction == player.Faction =>
ops.specialItemSlotGuid = Some(obj.GUID)
player.Carrying = SpecialCarry.CaptureFlag
continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player)
case None =>
log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}")
case Some(guid) if guid != obj.GUID =>
// Ignore duplicate pickup requests
log.warn(
s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid"
)
case _ => ()
}
}
private def handleUseWarpGate(equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
(continent.GUID(player.VehicleSeated), equipment) match {
case (Some(vehicle: Vehicle), Some(item))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
vehicle.Actor ! CommonMessages.Use(player, equipment)
case _ => ()
}
}
private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = {
equipment.foreach { item =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.Actor ! CommonMessages.Use(player, Some(item))
}
}
private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.Actor ! CommonMessages.Use(player, Some(equipment))
}
private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
equipment match {
case Some(item)
if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) ||
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => ()
case _ =>
log.warn(s"UseItem: ${player.Name} does not know how to handle $obj")
}
}
/**
* Get the current `Vehicle` object that the player is riding/driving.
* The vehicle must be found solely through use of `player.VehicleSeated`.
* @return the vehicle
*/
private def findLocalVehicle: Option[Vehicle] = {
continent.GUID(player.VehicleSeated) match {
case Some(obj: Vehicle) => Some(obj)
case _ => None
}
}
/**
* A simple object searching algorithm that is limited to containers currently known and accessible by the player.
* If all relatively local containers are checked and the object is not found,
* the player's locker inventory will be checked, and then
* the game environment (items on the ground) will be checked too.
* If the target object is discovered, it is removed from its current location and is completely destroyed.
* @see `RequestDestroyMessage`
* @see `Zone.ItemIs.Where`
* @param objectGuid the target object's globally unique identifier;
* it is not expected that the object will be unregistered, but it is also not gauranteed
* @param obj the target object
* @return `true`, if the target object was discovered and removed;
* `false`, otherwise
*/
private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = {
val findFunc
: PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
ops.findInLocalContainer(objectGuid)
findFunc(player)
.orElse(ops.accessedContainer match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
})
.orElse(findLocalVehicle match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
}) match {
case Some((parent, Some(_))) =>
obj.Position = Vector3.Zero
RemoveOldEquipmentFromInventory(parent)(obj)
true
case _ if player.avatar.locker.Inventory.Remove(objectGuid) =>
sendResponse(ObjectDeleteMessage(objectGuid, 0))
true
case _ if continent.EquipmentOnGround.contains(obj) =>
obj.Position = Vector3.Zero
continent.Ground ! Zone.Ground.RemoveItem(objectGuid)
continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
true
case _ =>
Zone.EquipmentIs.Where(obj, objectGuid, continent) match {
case None =>
true
case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID =>
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
true
case Some(Zone.EquipmentIs.Orphaned()) =>
true
case _ =>
log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it")
false
}
}
}
/**
* A player uses a fully-linked Router teleportation system.
* @param router the Router vehicle
* @param internalTelepad the internal telepad within the Router vehicle
* @param remoteTelepad the remote telepad that is currently associated with this Router
* @param src the origin of the teleportation (where the player starts)
* @param dest the destination of the teleportation (where the player is going)
*/
private def useRouterTelepadSystem(
router: Vehicle,
internalTelepad: InternalTelepad,
remoteTelepad: TelepadDeployable,
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
val time = System.currentTimeMillis()
if (
time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
val pguid = player.GUID
val sguid = src.GUID
val dguid = dest.GUID
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
ops.useRouterTelepadEffect(pguid, sguid, dguid)
continent.LocalEvents ! LocalServiceMessage(
continent.id,
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
)
val vSource = VehicleSource(router)
val zoneNumber = continent.Number
player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
player.Position = dest.Position
player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}
ops.recentTeleportAttempt = time
}
private def maxCapacitorTick(jumpThrust: Boolean): Unit = { private def maxCapacitorTick(jumpThrust: Boolean): Unit = {
if (player.ExoSuit == ExoSuitType.MAX) { if (player.ExoSuit == ExoSuitType.MAX) {
val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0
@ -1542,11 +987,4 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
) )
) )
} }
private def commonFacilityShieldCharging(obj: PlanetSideServerObject with BlockMapEntity): Unit = {
obj.Actor ! CommonMessages.ChargeShields(
15,
Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius))
)
}
} }

View file

@ -8,9 +8,9 @@ import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.vehicles.MountableWeapons import net.psforever.objects.vehicles.MountableWeapons
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable}
import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage}
import net.psforever.services.Service import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.local.LocalResponse import net.psforever.services.local.LocalResponse
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} import net.psforever.types.{ChatMessageType, PlanetSideGUID, SpawnGroup}
object LocalHandlerLogic { object LocalHandlerLogic {
def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
@ -88,7 +88,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
obj.Destroyed = true obj.Destroyed = true
DeconstructDeployable( ops.DeconstructDeployable(
obj, obj,
dguid, dguid,
pos, pos,
@ -102,7 +102,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
obj.Destroyed = true obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
//if active, deactivate //if active, deactivate
@ -117,7 +117,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act
ops.deactivateTelpadDeployableMessages(dguid) ops.deactivateTelpadDeployableMessages(dguid)
//standard deployable elimination behavior //standard deployable elimination behavior
obj.Destroyed = true obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
//standard deployable elimination behavior //standard deployable elimination behavior
@ -126,14 +126,14 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
//standard deployable elimination behavior //standard deployable elimination behavior
obj.Destroyed = true obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
sendResponse(ObjectDeleteMessage(dguid, unk1=0)) sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
obj.Destroyed = true obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2)) sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2))
@ -240,29 +240,28 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
} }
case LocalResponse.ForceZoneChange(zone) =>
//todo we might be able to piggyback this for squad recalls later
if(session.zone eq zone) {
sessionLogic.zoning.zoneReload = true
zone.AvatarEvents ! Service.Leave()
zone.LocalEvents ! Service.Leave()
zone.VehicleEvents ! Service.Leave()
zone.AvatarEvents ! Service.Join(player.Name) //must manually restore this subscriptions
sessionLogic.zoning.spawn.handleNewPlayerLoaded(player) //will restart subscriptions and dispatch a LoadMapMessage
} else {
import akka.actor.typed.scaladsl.adapter._
sessionLogic.cluster ! InterstellarClusterService.GetRandomSpawnPoint(
zone.Number,
player.Faction,
Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS),
context.self
)
}
case _ => () case _ => ()
} }
} }
/* support functions */ /* support functions */
/**
* Common behavior for deconstructing deployables in the game environment.
* @param obj the deployable
* @param guid the globally unique identifier for the deployable
* @param pos the previous position of the deployable
* @param orient the previous orientation of the deployable
* @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
*/
def DeconstructDeployable(
obj: Deployable,
guid: PlanetSideGUID,
pos: Vector3,
orient: Vector3,
deletionType: Int
): Unit = {
sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
sendResponse(ObjectDeleteMessage(guid, deletionType))
}
} }

View file

@ -1,8 +1,7 @@
// Copyright (c) 2024 PSForever // Copyright (c) 2024 PSForever
package net.psforever.actors.session.normal package net.psforever.actors.session.normal
import akka.actor.{ActorContext, typed} import akka.actor.ActorContext
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers} import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles} import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
@ -14,16 +13,14 @@ import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} import net.psforever.objects.vehicles.AccessPermissionGroup
import net.psforever.objects.vital.InGameHistory import net.psforever.objects.vital.InGameHistory
import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3} import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3}
import scala.concurrent.duration._
object MountHandlerLogic { object MountHandlerLogic {
def apply(ops: SessionMountHandlers): MountHandlerLogic = { def apply(ops: SessionMountHandlers): MountHandlerLogic = {
new MountHandlerLogic(ops, ops.context) new MountHandlerLogic(ops, ops.context)
@ -33,116 +30,22 @@ object MountHandlerLogic {
class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions { class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/* packets */ /* packets */
def handleMountVehicle(pkt: MountVehicleMsg): Unit = { def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt ops.handleMountVehicle(pkt)
sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect {
case obj: Mountable =>
obj.Actor ! Mountable.TryMount(player, entry_point)
case _ =>
log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
}
} }
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt ops.handleDismountVehicle(pkt)
val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
//TODO optimize this later
//common warning for this section
if (player.GUID == player_guid) {
//normally disembarking from a mount
(sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case out @ Some(obj: Vehicle) =>
continent.GUID(obj.MountedIn) match {
case Some(_: Vehicle) => None //cargo vehicle
case _ => out //arrangement "may" be permissible
}
case out @ Some(_: Mountable) =>
out
case _ =>
dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
None
}) match {
case Some(obj: Mountable) =>
obj.PassengerInSeat(player) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
sessionLogic.zoning.interstellarFerry = None
case None =>
dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
}
case _ =>
dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
}
} else {
//kicking someone else out of a mount; need to own that mount/mountable
val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
player.avatar.vehicle match {
case Some(obj_guid) =>
(
(
sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
) match {
case (vehicle @ Some(obj: Vehicle), tplayer) =>
if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
case (mount @ Some(_: Mountable), tplayer) =>
(mount, tplayer)
case _ =>
(None, None)
}) match {
case (Some(obj: Mountable), Some(tplayer: Player)) =>
obj.PassengerInSeat(tplayer) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
case None =>
dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
}
case (None, _) =>
dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
case (_, None) =>
dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
case _ =>
dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
}
case None =>
dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
}
}
} }
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt ops.handleMountVehicleCargo(pkt)
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
case Some((mountPoint, _)) =>
cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
case _ =>
log.warn(
s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
)
}
case (None, _) | (Some(_), None) =>
log.warn(
s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
)
case _ => ()
}
} }
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt ops.handleDismountVehicleCargo(pkt)
continent.GUID(cargo_guid) match {
case Some(cargo: Vehicle) =>
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
case _ => ()
}
} }
/* response handlers */ /* response handlers */
@ -156,24 +59,24 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
reply match { reply match {
case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) => case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
log.info(s"${player.Name} mounts an implant terminal") log.info(s"${player.Name} mounts an implant terminal")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.orbital_shuttle => if obj.Definition == GlobalDefinitions.orbital_shuttle =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the orbital shuttle") log.info(s"${player.Name} mounts the orbital shuttle")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.ant => if obj.Definition == GlobalDefinitions.ant =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
@ -182,12 +85,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition == GlobalDefinitions.quadstealth => if obj.Definition == GlobalDefinitions.quadstealth =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
@ -198,12 +101,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 => if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
@ -213,12 +116,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if seatNumber == 0 => if seatNumber == 0 =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
@ -227,17 +130,17 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition.MaxCapacitor > 0 => if obj.Definition.MaxCapacitor > 0 =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts ${ log.info(s"${player.Name} mounts ${
obj.SeatPermissionGroup(seatNumber) match { obj.SeatPermissionGroup(seatNumber) match {
case Some(seatType) => s"a $seatType seat (#$seatNumber)" case Some(seatType) => s"a $seatType seat (#$seatNumber)"
case None => "a seat" case None => "a seat"
} }
} of the ${obj.Definition.Name}") } of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
@ -245,71 +148,71 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) => case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the ${ log.info(s"${player.Name} mounts the ${
obj.SeatPermissionGroup(seatNumber) match { obj.SeatPermissionGroup(seatNumber) match {
case Some(seatType) => s"a $seatType seat (#$seatNumber)" case Some(seatType) => s"a $seatType seat (#$seatNumber)"
case None => "a seat" case None => "a seat"
} }
} of the ${obj.Definition.Name}") } of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if obj.Definition == GlobalDefinitions.vanu_sentry_turret => if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the ${obj.Definition.Name}") log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction)) obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
obj.setMiddleOfUpgrade(false) obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, _, _) => case Mountable.CanMount(obj: FacilityTurret, _, _) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.warn( log.warn(
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating" s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
) )
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) => case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}") log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Mountable, _, _) => case Mountable.CanMount(obj: Mountable, _, _) =>
log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}") log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
log.info(s"${tplayer.Name} dismounts the implant terminal") log.info(s"${tplayer.Name} dismounts the implant terminal")
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, _, mountPoint) case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty => if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
//dismount to hart lobby //dismount to hart lobby
val pguid = player.GUID val pguid = player.GUID
log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
val sguid = obj.GUID val sguid = obj.GUID
val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint) val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
tplayer.Position = pos tplayer.Position = pos
@ -322,11 +225,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
case Mountable.CanDismount(obj: Vehicle, seatNum, _) case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if obj.Definition == GlobalDefinitions.orbital_shuttle => if obj.Definition == GlobalDefinitions.orbital_shuttle =>
log.info(s"${player.Name} is prepped for dropping")
//get ready for orbital drop //get ready for orbital drop
val pguid = player.GUID val pguid = player.GUID
val events = continent.VehicleEvents val events = continent.VehicleEvents
log.info(s"${player.Name} is prepped for dropping") ops.DismountAction(tplayer, obj, seatNum)
DismountAction(tplayer, obj, seatNum)
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
events ! VehicleServiceMessage( events ! VehicleServiceMessage(
@ -354,7 +257,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
if obj.Definition == GlobalDefinitions.droppod => if obj.Definition == GlobalDefinitions.droppod =>
log.info(s"${tplayer.Name} has landed on ${continent.id}") log.info(s"${tplayer.Name} has landed on ${continent.id}")
sessionLogic.general.unaccessContainer(obj) sessionLogic.general.unaccessContainer(obj)
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
obj.Actor ! Vehicle.Deconstruct() obj.Actor ! Vehicle.Deconstruct()
case Mountable.CanDismount(obj: Vehicle, seatNum, _) case Mountable.CanDismount(obj: Vehicle, seatNum, _)
@ -365,12 +268,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
//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.
//todo: kick cargo passengers out. To be added after PR #216 is merged //todo: kick cargo passengers out. To be added after PR #216 is merged
DismountVehicleAction(tplayer, obj, seatNum) ops.DismountVehicleAction(tplayer, obj, seatNum)
obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
case Mountable.CanDismount(obj: Vehicle, seatNum, _) case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if tplayer.GUID == player.GUID => if tplayer.GUID == player.GUID =>
DismountVehicleAction(tplayer, obj, seatNum) ops.DismountVehicleAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, seat_num, _) => case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
@ -380,7 +283,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
case Mountable.CanDismount(obj: PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => case Mountable.CanDismount(obj: PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Mountable, _, _) => case Mountable.CanDismount(obj: Mountable, _, _) =>
log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}") log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
@ -409,6 +312,10 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
if obj.DeploymentState == DriveState.AutoPilot => if obj.DeploymentState == DriveState.AutoPilot =>
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime"))
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.Health <= (obj.MaxHealth * .35).round && GlobalDefinitions.isFlightVehicle(obj.Definition) =>
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@BailingMechanismFailure_Pilot"))
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if { if {
continent continent
@ -434,118 +341,4 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
} }
/* support functions */ /* support functions */
private def dismountWarning(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.warn(note)
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
private def dismountError(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
/**
* Common activities/procedure when a player mounts a valid object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount into which the player is mounting
*/
private def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID
val objGuid: PlanetSideGUID = obj.GUID
sessionLogic.actionsToCancel()
avatarActor ! AvatarActor.DeactivateActiveImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
)
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
private def DismountVehicleAction(tplayer: Player, obj: Vehicle, seatNum: Int): Unit = {
//disembarking self
log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
obj.SeatPermissionGroup(seatNum) match {
case Some(AccessPermissionGroup.Driver) => "driver seat"
case Some(seatType) => s"$seatType seat (#$seatNum)"
case None => "seat"
}
}")
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
sessionLogic.general.unaccessContainer(obj)
DismountAction(tplayer, obj, seatNum)
//until vehicles maintain synchronized momentum without a driver
obj match {
case v: Vehicle
if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
sessionLogic.vehicles.ServerVehicleOverrideStop(v)
}
v.Velocity = Vector3.Zero
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
tplayer.GUID,
v.GUID,
unk1 = 0,
v.Position,
v.Orientation,
vel = None,
v.Flying,
unk3 = 0,
unk4 = 0,
wheel_direction = 15,
unk5 = false,
unk6 = v.Cloaked
)
)
case _ => ()
}
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID
tplayer.ContributionFrom(obj)
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
val bailType = if (tplayer.BailProtection) {
BailType.Bailed
} else {
BailType.Normal
}
sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
)
}
} }

View file

@ -1,13 +1,12 @@
// Copyright (c) 2024 PSForever // Copyright (c) 2024 PSForever
package net.psforever.actors.session.normal package net.psforever.actors.session.normal
import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} import net.psforever.actors.session.support.{ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
class NormalModeLogic(data: SessionData) extends ModeLogic { class NormalModeLogic(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse) val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
val chat: ChatFunctions = ChatLogic(data.chat) val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers) val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general) val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)

View file

@ -29,22 +29,20 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
private val squadService: ActorRef = ops.squadService private val squadService: ActorRef = ops.squadService
private var waypointCooldown: Long = 0L
/* packet */ /* packet */
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
val SquadDefinitionActionMessage(u1, u2, action) = pkt /*val SquadDefinitionActionMessage(u1, u2, action) = pkt
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))*/
} }
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt /*val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
squadService ! SquadServiceMessage( squadService ! SquadServiceMessage(
player, player,
continent, continent,
SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
) )*/
} }
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {

View file

@ -5,14 +5,11 @@ import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions} import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions}
import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.guid.TaskWorkflow import net.psforever.objects.guid.TaskWorkflow
import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage}
import net.psforever.objects.sourcing.AmenitySource import net.psforever.types.TransactionType
import net.psforever.objects.vital.TerminalUsedActivity
import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage}
import net.psforever.types.{TransactionType, Vector3}
object TerminalHandlerLogic { object TerminalHandlerLogic {
def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = { def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
@ -26,46 +23,17 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
continent.GUID(terminalGuid) match {
case Some(term: Terminal) if ops.lastTerminalOrderFulfillment =>
val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
ops.lastTerminalOrderFulfillment = false
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
term.Actor ! Terminal.Request(player, pkt) ops.handleItemTransaction(pkt)
case Some(_: Terminal) =>
log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
case Some(obj) =>
log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
case _ =>
log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
}
} }
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
val ProximityTerminalUseMessage(_, objectGuid, _) = pkt ops.handleProximityTerminalUse(pkt)
continent.GUID(objectGuid) match {
case Some(obj: Terminal with ProximityUnit) =>
ops.HandleProximityTerminalUse(obj)
case Some(obj) =>
log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
case None =>
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
}
} }
def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
val FavoritesRequest(_, loadoutType, action, line, label) = pkt
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
action match { ops.handleFavoritesRequest(pkt)
case FavoritesAction.Save =>
avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
case FavoritesAction.Delete =>
avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)
case FavoritesAction.Unknown =>
log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action")
}
} }
/** /**
@ -117,48 +85,10 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex
ops.lastTerminalOrderFulfillment = true ops.lastTerminalOrderFulfillment = true
case Terminal.BuyVehicle(vehicle, weapons, trunk) => case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
continent.map.terminalToSpawnPad ops.buyVehicle(msg.terminal_guid, msg.transaction_type, vehicle, weapons, trunk)
.find { case (termid, _) => termid == msg.terminal_guid.guid } .collect {
.map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } case _: Vehicle =>
.collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition)
vehicle.Faction = tplayer.Faction
vehicle.Position = pad.Position
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
//default loadout, weapons
val vWeapons = vehicle.Weapons
weapons.foreach { entry =>
vWeapons.get(entry.start) match {
case Some(slot) =>
entry.obj.Faction = tplayer.Faction
slot.Equipment = None
slot.Equipment = entry.obj
case None =>
log.warn(
s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
)
}
}
//default loadout, trunk
val vTrunk = vehicle.Trunk
vTrunk.Clear()
trunk.foreach { entry =>
entry.obj.Faction = tplayer.Faction
vTrunk.InsertQuickly(entry.start, entry.obj)
}
TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term))
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true))
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
}
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
}
.orElse {
log.error(
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
)
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
None
} }
ops.lastTerminalOrderFulfillment = true ops.lastTerminalOrderFulfillment = true

View file

@ -8,6 +8,7 @@ import net.psforever.objects.avatar.SpecialCarry
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
@ -187,6 +188,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
s"${player.Sex.possessive} ride" s"${player.Sex.possessive} ride"
} }
log.info(s"${player.Name} has been kicked from $typeOfRide!") log.info(s"${player.Name} has been kicked from $typeOfRide!")
player.WhichSide = OutsideOf
case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) => case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
//seat number (first field) seems to be correct if passenger is kicked manually by driver //seat number (first field) seems to be correct if passenger is kicked manually by driver
@ -229,7 +231,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData)) sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
case VehicleResponse.UnloadVehicle(_, vehicleGuid) => case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1))
if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists { if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists {
case ams: Vehicle => case ams: Vehicle =>
ams.GUID == vehicleGuid && ams.GUID == vehicleGuid &&

View file

@ -216,7 +216,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
case _ => (None, None) case _ => (None, None)
}) match { }) match {
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
()
case _ => case _ =>
sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals?
sessionLogic.persist() sessionLogic.persist()

View file

@ -221,7 +221,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.HitHint(sourceGuid) if player.isAlive => case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
sendResponse(HitHint(sourceGuid, guid)) sendResponse(HitHint(sourceGuid, guid))
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") sessionLogic.zoning.CancelZoningProcess()
case AvatarResponse.Destroy(victim, killer, weapon, pos) => case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer // guid = victim // killer = killer
@ -235,10 +235,6 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
sessionLogic.terminals.lastTerminalOrderFulfillment = true sessionLogic.terminals.lastTerminalOrderFulfillment = true
AvatarActor.savePlayerData(player) AvatarActor.savePlayerData(player)
sessionLogic.general.renewCharSavedTimer(
Config.app.game.savedMsg.interruptedByAction.fixed,
Config.app.game.savedMsg.interruptedByAction.variable
)
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) => case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
@ -430,7 +426,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
case AvatarResponse.Killed(mount) => case AvatarResponse.Killed(_, mount) =>
//log and chat messages //log and chat messages
val cause = player.LastDamage.flatMap { damage => val cause = player.LastDamage.flatMap { damage =>
val interaction = damage.interaction val interaction = damage.interaction
@ -451,8 +447,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
) )
sessionLogic.shooting.shotsWhileDead = 0 sessionLogic.shooting.shotsWhileDead = 0
} }
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") sessionLogic.zoning.CancelZoningProcess()
sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes //player state changes
sessionLogic.zoning.spawn.avatarActive = false sessionLogic.zoning.spawn.avatarActive = false
@ -516,7 +511,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.EnvironmentalDamage(_, _, _) => case AvatarResponse.EnvironmentalDamage(_, _, _) =>
//TODO damage marker? //TODO damage marker?
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") sessionLogic.zoning.CancelZoningProcess()
case AvatarResponse.DropItem(pkt) if isNotSameTarget => case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
sendResponse(pkt) sendResponse(pkt)

View file

@ -9,6 +9,8 @@ import net.psforever.objects.Session
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
import net.psforever.services.chat.SpectatorChannel import net.psforever.services.chat.SpectatorChannel
import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType
import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM}
import net.psforever.zones.Zones
import scala.collection.Seq import scala.collection.Seq
@ -19,6 +21,8 @@ object ChatLogic {
} }
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
ops.transitoryCommandEntered = None
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
def handleChatMsg(message: ChatMsg): Unit = { def handleChatMsg(message: ChatMsg): Unit = {
@ -31,11 +35,10 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_FLY, recipient, contents) => case (CMT_FLY, recipient, contents) =>
ops.commandFly(contents, recipient) ops.commandFly(contents, recipient)
case (CMT_ANONYMOUS, _, _) => case (CMT_ANONYMOUS, _, _) => ()
// ?
case (CMT_TOGGLE_GM, _, _) => case (CMT_TOGGLE_GM, _, _) => ()
// ? sessionLogic.zoning.maintainInitialGmState = false
case (CMT_CULLWATERMARK, _, contents) => case (CMT_CULLWATERMARK, _, contents) =>
ops.commandWatermark(contents) ops.commandWatermark(contents)
@ -56,19 +59,19 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
commandToggleSpectatorMode(contents = "off") commandToggleSpectatorMode(contents = "off")
case (CMT_OPEN, _, _) => case (CMT_OPEN, _, _) =>
ops.commandSendToRecipient(session, message, SpectatorChannel) ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel)
case (CMT_VOICE, _, contents) => case (CMT_VOICE, _, contents) =>
ops.commandVoice(session, message, contents, SpectatorChannel) ops.commandVoice(session, message, contents, SpectatorChannel)
case (CMT_TELL, _, _) => case (CMT_TELL, _, _) =>
ops.commandTellOrIgnore(session, message, SpectatorChannel) ops.commandTellOrIgnore(session, spectatorColoredMessage(message), SpectatorChannel)
case (CMT_BROADCAST, _, _) => case (CMT_BROADCAST, _, _) =>
ops.commandSendToRecipient(session, message, SpectatorChannel) ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel)
case (CMT_PLATOON, _, _) => case (CMT_PLATOON, _, _) =>
ops.commandSendToRecipient(session, message, SpectatorChannel) ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel)
case (CMT_GMTELL, _, _) => case (CMT_GMTELL, _, _) =>
ops.commandSend(session, message, SpectatorChannel) ops.commandSend(session, message, SpectatorChannel)
@ -79,8 +82,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
ops.commandWho(session) ops.commandWho(session)
case (CMT_ZONE, _, contents) => case (CMT_ZONE, _, _) =>
ops.commandZone(message, contents) commandToggleSpectatorMode(contents = "off")
ops.commandZone(message, Zones.sanctuaryZoneId(player.Faction))
case (CMT_WARP, _, contents) => case (CMT_WARP, _, contents) =>
ops.commandWarp(session, message, contents) ops.commandWarp(session, message, contents)
@ -115,6 +119,16 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
} }
} }
private def spectatorColoredMessage(message: ChatMsg): ChatMsg = {
if (message.contents.nonEmpty) {
val colorlessText = message.contents.replaceAll("//#\\d", "").trim
val colorCodedText = s"/#5$colorlessText/#0"
message.copy(recipient = s"<spectator:${message.recipient}>", contents = colorCodedText)
} else {
message
}
}
private def customCommandMessages( private def customCommandMessages(
message: ChatMsg, message: ChatMsg,
session: Session session: Session
@ -126,21 +140,42 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case _ => ("", Seq("")) case _ => ("", Seq(""))
} }
command match { command match {
case "list" => ops.customCommandList(session, params, message) case "list" => ops.customCommandList(session, params.toSeq, message)
case "nearby" => ops.customCommandNearby(session) case "nearby" => ops.customCommandNearby(session)
case "loc" => ops.customCommandLoc(session, message) case "loc" => ops.customCommandLoc(session, message)
case _ => false case _ =>
// command was not handled
sendResponse(
ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
true
} }
} else { } else {
false false
} }
} }
private def commandToggleSpectatorMode(contents: String): Unit = { private def commandToggleSpectatorMode(contents: String): Boolean = {
ops.transitoryCommandEntered
.collect {
case CMT_TOGGLESPECTATORMODE => true
case CMT_TOGGLE_GM => false
}
.getOrElse {
contents.toLowerCase() match { contents.toLowerCase() match {
case "off" | "of" => case "off" | "of" =>
ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE)
context.self ! SessionActor.SetMode(NormalMode) context.self ! SessionActor.SetMode(NormalMode)
case _ => () true
case _ =>
false
}
} }
} }
} }

View file

@ -1,85 +0,0 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData}
import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage}
import net.psforever.types.{MemberAction, PlanetSideEmpire}
object GalaxyHandlerLogic {
def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = {
new GalaxyHandlerLogic(ops, ops.context)
}
}
class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
private val galaxyService: ActorRef = ops.galaxyService
/* packets */
def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = {
sendResponse(pkt)
pkt.friends.foreach { f =>
galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
}
}
/* response handlers */
def handle(reply: GalaxyResponse.Response): Unit = {
reply match {
case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
sendResponse(
HotSpotUpdateMessage(
zone_index,
priority,
hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
)
)
case GalaxyResponse.MapUpdate(msg) =>
sendResponse(msg)
case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
val faction = player.Faction
val from = fromFactions.contains(faction)
val to = toFactions.contains(faction)
if (from && !to) {
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
} else if (!from && to) {
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
}
case GalaxyResponse.FlagMapUpdate(msg) =>
sendResponse(msg)
case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
case GalaxyResponse.UnlockedZoneUpdate(zone) =>
sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
val popBO = 0
val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) =>
avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
case GalaxyResponse.SendResponse(msg) =>
sendResponse(msg)
case _ => ()
}
}
}

View file

@ -4,24 +4,19 @@ package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, ActorRef, typed} import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle} import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, Player, TelepadDeployable, Tool, Vehicle}
import net.psforever.objects.avatar.{Avatar, Implant} import net.psforever.objects.avatar.{Avatar, Implant}
import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.{Deployable, TelepadLike}
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.zones.ZoneProjectile import net.psforever.objects.zones.ZoneProjectile
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.account.AccountPersistenceService import net.psforever.services.account.AccountPersistenceService
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.util.Config
object GeneralLogic { object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = { def apply(ops: GeneralOperations): GeneralLogic = {
@ -52,7 +47,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
yaw, yaw,
pitch, pitch,
yawUpper, yawUpper,
_/*seqTime*/, seqTime,
_, _,
isCrouching, isCrouching,
isJumping, isJumping,
@ -64,6 +59,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.turnCounterFunc(avatarGuid)
sessionLogic.updateBlockMap(player, pos)
ops.fallHeightTracker(pos.z) ops.fallHeightTracker(pos.z)
// if (isCrouching && !player.Crouching) { // if (isCrouching && !player.Crouching) {
// //dev stuff goes here // //dev stuff goes here
@ -75,6 +71,24 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
player.Crouching = isCrouching player.Crouching = isCrouching
player.Jumping = isJumping player.Jumping = isJumping
player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
continent.AvatarEvents ! AvatarServiceMessage(
"spectator",
AvatarAction.PlayerState(
avatarGuid,
player.Position,
player.Velocity,
yaw,
pitch,
yawUpper,
seqTime,
isCrouching,
isJumping,
jump_thrust = false,
is_cloaked = isCloaking,
spectator = false,
weaponInHand = false
)
)
if (player.death_by == -1) { if (player.death_by == -1) {
sessionLogic.kickedByAdministration() sessionLogic.kickedByAdministration()
} }
@ -107,31 +121,15 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* intentionally blank */ } def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* intentionally blank */ }
def handleZipLine(pkt: ZipLineMessage): Unit = { def handleZipLine(pkt: ZipLineMessage): Unit = {
val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt ops.handleZipLine(pkt) match {
continent.zipLinePaths.find(x => x.PathId == pathId) match { case GeneralOperations.ZiplineBehavior.Teleporter | GeneralOperations.ZiplineBehavior.Zipline =>
case Some(path) if path.IsTeleporter => sessionLogic.zoning.CancelZoningProcess()
val endPoint = path.ZipLinePoints.last case GeneralOperations.ZiplineBehavior.Unsupported =>
sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
//todo: send to zone to show teleport animation to all clients
sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
case Some(_) =>
action match {
case 0 =>
//travel along the zipline in the direction specified
sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
case 1 =>
//disembark from zipline at destination!
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
case 2 =>
//get off by force
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
case _ =>
log.warn( log.warn(
s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" s"${player.Name} tried to do something with a zipline but can't handle it. action: ${pkt.action}, pathId: ${pkt.path_id}, zone: ${continent.id}"
) )
} case GeneralOperations.ZiplineBehavior.NotFound =>
case _ => log.warn(s"${player.Name} couldn't find a zipline path ${pkt.path_id} in zone ${continent.id}")
log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
} }
} }
@ -169,11 +167,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleUseItem(pkt: UseItemMessage): Unit = { def handleUseItem(pkt: UseItemMessage): Unit = {
sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
case Some(door: Door) => case Some(door: Door) =>
handleUseDoor(door, None) ops.handleUseDoor(door, None)
case Some(obj: TelepadDeployable) => case Some(obj: TelepadDeployable) =>
handleUseTelepadDeployable(obj, None, pkt) ops.handleUseTelepadDeployable(obj, None, pkt, ops.useRouterTelepadSystemSecretly)
case Some(obj: Utility.InternalTelepad) => case Some(obj: Utility.InternalTelepad) =>
handleUseInternalTelepad(obj, pkt) ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystemSecretly)
case _ => () case _ => ()
} }
} }
@ -268,15 +266,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
case GenericAction.AwayFromKeyboard_RCV => case GenericAction.AwayFromKeyboard_RCV =>
log.info(s"${player.Name} is AFK") log.info(s"${player.Name} is AFK")
AvatarActor.savePlayerLocation(player) AvatarActor.savePlayerLocation(player)
ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
player.AwayFromKeyboard = true player.AwayFromKeyboard = true
case GenericAction.BackInGame_RCV => case GenericAction.BackInGame_RCV =>
log.info(s"${player.Name} is back") log.info(s"${player.Name} is back")
player.AwayFromKeyboard = false player.AwayFromKeyboard = false
ops.renewCharSavedTimer(
Config.app.game.savedMsg.renewal.fixed,
Config.app.game.savedMsg.renewal.variable
)
case GenericAction.LookingForSquad_RCV => //Looking For Squad ON case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
avatarActor ! AvatarActor.SetLookingForSquad(true) avatarActor ! AvatarActor.SetLookingForSquad(true)
@ -370,34 +363,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleTrade(pkt: TradeMessage): Unit = { /* intentionally blank */ } def handleTrade(pkt: TradeMessage): Unit = { /* intentionally blank */ }
def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { /* intentionally blank */ }
val DisplayedAwardMessage(_, ribbon, bar) = pkt
log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
}
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { /* intentionally blank */ } def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
ops.handleObjectDetected(pkt)
}
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
val TargetingImplantRequest(list) = pkt ops.handleTargetingImplantRequest(pkt)
val targetInfo: List[TargetInfo] = list.flatMap { x =>
continent.GUID(x.target_guid) match {
case Some(player: Player) =>
val health = player.Health.toFloat / player.MaxHealth
val armor = if (player.MaxArmor > 0) {
player.Armor.toFloat / player.MaxArmor
} else {
0
}
Some(TargetInfo(player.GUID, health, armor))
case _ =>
log.warn(
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
)
None
}
}
sendResponse(TargetingInfoMessage(targetInfo))
} }
def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ } def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ }
@ -445,8 +418,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
def handleKick(player: Player, time: Option[Long]): Unit = { def handleKick(player: Player, time: Option[Long]): Unit = {
administrativeKick(player) ops.administrativeKick(player, None)
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
} }
def handleSilenced(isSilenced: Boolean): Unit = { def handleSilenced(isSilenced: Boolean): Unit = {
@ -461,107 +433,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
/* supporting functions */ /* supporting functions */
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)
}
}
private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
if (equipment.isEmpty) {
(continent.GUID(obj.Router) match {
case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
case Some(vehicle) => Some(vehicle, None)
case None => None
}) match {
case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
player.WhichSide = vehicle.WhichSide
useRouterTelepadSystem(
router = vehicle,
internalTelepad = util,
remoteTelepad = obj,
src = obj,
dest = util
)
case Some((vehicle: Vehicle, None)) =>
log.error(
s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
)
case Some((o, _)) =>
log.error(
s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}"
)
obj.Actor ! Deployable.Deconstruct()
case _ => ()
}
}
}
private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
continent.GUID(obj.Telepad) match {
case Some(pad: TelepadDeployable) =>
player.WhichSide = pad.WhichSide
useRouterTelepadSystem(
router = obj.Owner.asInstanceOf[Vehicle],
internalTelepad = obj,
remoteTelepad = pad,
src = obj,
dest = pad
)
case Some(o) =>
log.error(
s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}"
)
case None => ()
}
}
/**
* A player uses a fully-linked Router teleportation system.
* @param router the Router vehicle
* @param internalTelepad the internal telepad within the Router vehicle
* @param remoteTelepad the remote telepad that is currently associated with this Router
* @param src the origin of the teleportation (where the player starts)
* @param dest the destination of the teleportation (where the player is going)
*/
private def useRouterTelepadSystem(
router: Vehicle,
internalTelepad: InternalTelepad,
remoteTelepad: TelepadDeployable,
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
val time = System.currentTimeMillis()
if (
time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
val pguid = player.GUID
val sguid = src.GUID
val dguid = dest.GUID
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
ops.useRouterTelepadEffect(pguid, sguid, dguid)
player.Position = dest.Position
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}
ops.recentTeleportAttempt = time
}
private def administrativeKick(tplayer: Player): Unit = {
log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
tplayer.death_by = -1
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
}
private def customImplantOff(slot: Int, implant: Implant): Unit = { private def customImplantOff(slot: Int, implant: Implant): Unit = {
customImplants = customImplants.updated(slot, implant.copy(active = false)) customImplants = customImplants.updated(slot, implant.copy(active = false))
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0)) sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0))

View file

@ -1,257 +0,0 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.spectator
import akka.actor.ActorContext
import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.vehicles.MountableWeapons
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable}
import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage}
import net.psforever.services.Service
import net.psforever.services.local.LocalResponse
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
object LocalHandlerLogic {
def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
new LocalHandlerLogic(ops, ops.context)
}
}
class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic
/* messages */
def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = {
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj))
}
def handleDeployableIsDismissed(obj: Deployable): Unit = {
TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj))
}
/* response handlers */
/**
* na
* @param toChannel na
* @param guid na
* @param reply na
*/
def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
val resolvedPlayerGuid = if (player.HasGUID) {
player.GUID
} else {
Service.defaultPlayerGUID
}
val isNotSameTarget = resolvedPlayerGuid != guid
reply match {
case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget =>
sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
case LocalResponse.DeployableUIFor(item) =>
sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
sendResponse(GenericObjectActionMessage(dguid, code=19))
sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.Detonate(_, obj) =>
log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget =>
sendResponse(GenericObjectStateMsg(doorGuid, state=16))
case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone
sendResponse(GenericObjectStateMsg(doorGuid, state=17))
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed =>
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
obj.Destroyed = true
DeconstructDeployable(
obj,
dguid,
pos,
obj.Orientation,
deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 }
)
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _)
if obj.Destroyed || obj.Jammed || obj.Health == 0 =>
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
//if active, deactivate
obj.Active = false
ops.deactivateTelpadDeployableMessages(dguid)
//standard deployable elimination behavior
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active =>
//if active, deactivate
obj.Active = false
ops.deactivateTelpadDeployableMessages(dguid)
//standard deployable elimination behavior
obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
//standard deployable elimination behavior
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
//standard deployable elimination behavior
obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
obj.Destroyed = true
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2))
case LocalResponse.HackObject(targetGuid, unk1, unk2) =>
sessionLogic.general.hackObject(targetGuid, unk1, unk2)
case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue)
case LocalResponse.GenericObjectAction(targetGuid, actionNumber) =>
sendResponse(GenericObjectActionMessage(targetGuid, actionNumber))
case LocalResponse.GenericActionMessage(actionNumber) =>
sendResponse(GenericActionMessage(actionNumber))
case LocalResponse.ChatMessage(msg) =>
sendResponse(msg)
case LocalResponse.SendPacket(packet) =>
sendResponse(packet)
case LocalResponse.LluSpawned(llu) =>
// Create LLU on client
sendResponse(ObjectCreateMessage(
llu.Definition.ObjectId,
llu.GUID,
llu.Definition.Packet.ConstructorData(llu).get
))
sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f))
case LocalResponse.LluDespawned(lluGuid, position) =>
sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f))
sendResponse(ObjectDeleteMessage(lluGuid, unk1=0))
// If the player was holding the LLU, remove it from their tracked special item slot
sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
sessionLogic.general.specialItemSlotGuid = None
player.Carrying = None
}
case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget =>
sendResponse(ObjectDeleteMessage(objectGuid, unk))
case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true))
case LocalResponse.ProximityTerminalEffect(objectGuid, false) =>
sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false))
sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid)
case LocalResponse.RouterTelepadMessage(msg) =>
sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
case LocalResponse.SendResponse(msg) =>
sendResponse(msg)
case LocalResponse.SetEmpire(objectGuid, empire) =>
sendResponse(SetEmpireMessage(objectGuid, empire))
case LocalResponse.ShuttleEvent(ev) =>
val msg = OrbitalShuttleTimeMsg(
ev.u1,
ev.u2,
ev.t1,
ev.t2,
ev.t3,
pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) }
)
sendResponse(msg)
case LocalResponse.ShuttleDock(pguid, sguid, slot) =>
sendResponse(ObjectAttachMessage(pguid, sguid, slot))
case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) =>
sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient))
case LocalResponse.ShuttleState(sguid, pos, orient, state) =>
sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false))
case LocalResponse.ToggleTeleportSystem(router, systemPlan) =>
sessionLogic.general.toggleTeleportSystem(router, systemPlan)
case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) =>
sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation))
case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) =>
sendResponse(GenericObjectActionMessage(buildingGuid, 11))
case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) =>
sendResponse(GenericObjectActionMessage(buildingGuid, 12))
case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid =>
continent.GUID(vehicleGuid)
.collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
.collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
.getOrElse(Set.empty)
.collect { case weapon: Tool if weapon.GUID == weaponGuid =>
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
}
case _ => ()
}
}
/* support functions */
/**
* Common behavior for deconstructing deployables in the game environment.
* @param obj the deployable
* @param guid the globally unique identifier for the deployable
* @param pos the previous position of the deployable
* @param orient the previous orientation of the deployable
* @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
*/
def DeconstructDeployable(
obj: Deployable,
guid: PlanetSideGUID,
pos: Vector3,
orient: Vector3,
deletionType: Int
): Unit = {
sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
sendResponse(ObjectDeleteMessage(guid, deletionType))
}
}

View file

@ -10,13 +10,11 @@ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, V
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
import net.psforever.objects.vital.InGameHistory import net.psforever.objects.vital.InGameHistory
import net.psforever.packet.game.{DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} import net.psforever.packet.game.{DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, PlanetSideGUID, Vector3}
object MountHandlerLogic { object MountHandlerLogic {
def apply(ops: SessionMountHandlers): MountHandlerLogic = { def apply(ops: SessionMountHandlers): MountHandlerLogic = {
@ -29,98 +27,16 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
/* packets */ /* packets */
def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* intentionally blank */ } def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* can not mount as spectator */ }
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt ops.handleDismountVehicle(pkt)
val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
//TODO optimize this later
//common warning for this section
if (player.GUID == player_guid) {
//normally disembarking from a mount
(sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case out @ Some(obj: Vehicle) =>
continent.GUID(obj.MountedIn) match {
case Some(_: Vehicle) => None //cargo vehicle
case _ => out //arrangement "may" be permissible
}
case out @ Some(_: Mountable) =>
out
case _ =>
dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
None
}) match {
case Some(obj: Mountable) =>
obj.PassengerInSeat(player) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
sessionLogic.zoning.interstellarFerry = None
// 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: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
//todo: kick cargo passengers out. To be added after PR #216 is merged
obj match {
case v: Vehicle
if bailType == BailType.Bailed &&
v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) &&
v.isFlying =>
v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
case _ => ()
} }
case None => def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* can not mount as spectator */ }
dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
}
case _ =>
dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
}
} else {
//kicking someone else out of a mount; need to own that mount/mountable
val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
player.avatar.vehicle match {
case Some(obj_guid) =>
(
(
sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
) match {
case (vehicle @ Some(obj: Vehicle), tplayer) =>
if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
case (mount @ Some(_: Mountable), tplayer) =>
(mount, tplayer)
case _ =>
(None, None)
}) match {
case (Some(obj: Mountable), Some(tplayer: Player)) =>
obj.PassengerInSeat(tplayer) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
case None =>
dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
}
case (None, _) =>
dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
case (_, None) =>
dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
case _ =>
dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
}
case None =>
dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
}
}
}
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* intentionally blank */ }
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt ops.handleDismountVehicleCargo(pkt)
continent.GUID(cargo_guid) match {
case Some(cargo: Vehicle) =>
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
case _ => ()
}
} }
/* response handlers */ /* response handlers */
@ -134,7 +50,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
reply match { reply match {
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player) obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
case Mountable.CanDismount(obj: Vehicle, _, mountPoint) case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
@ -157,7 +73,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
//get ready for orbital drop //get ready for orbital drop
val pguid = player.GUID val pguid = player.GUID
val events = continent.VehicleEvents val events = continent.VehicleEvents
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
events ! VehicleServiceMessage( events ! VehicleServiceMessage(
@ -185,14 +101,14 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
case Mountable.CanDismount(obj: Vehicle, seatNum, _) case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if obj.Definition == GlobalDefinitions.droppod => if obj.Definition == GlobalDefinitions.droppod =>
sessionLogic.general.unaccessContainer(obj) sessionLogic.general.unaccessContainer(obj)
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
obj.Actor ! Vehicle.Deconstruct() obj.Actor ! Vehicle.Deconstruct()
case Mountable.CanDismount(obj: Vehicle, seatNum, _) case Mountable.CanDismount(obj: Vehicle, seatNum, _)
if tplayer.GUID == player.GUID => if tplayer.GUID == player.GUID =>
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
sessionLogic.general.unaccessContainer(obj) sessionLogic.general.unaccessContainer(obj)
DismountVehicleAction(tplayer, obj, seatNum) ops.DismountVehicleAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, seat_num, _) => case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
@ -201,7 +117,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
) )
case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
DismountAction(tplayer, obj, seatNum) ops.DismountAction(tplayer, obj, seatNum)
case Mountable.CanDismount(_: Mountable, _, _) => () case Mountable.CanDismount(_: Mountable, _, _) => ()
@ -213,90 +129,4 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
} }
/* support functions */ /* support functions */
private def dismountWarning(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.warn(note)
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
private def dismountError(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
DismountAction(tplayer, obj, seatNum)
//until vehicles maintain synchronized momentum without a driver
obj match {
case v: Vehicle
if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
sessionLogic.vehicles.ServerVehicleOverrideStop(v)
}
v.Velocity = Vector3.Zero
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
tplayer.GUID,
v.GUID,
unk1 = 0,
v.Position,
v.Orientation,
vel = None,
v.Flying,
unk3 = 0,
unk4 = 0,
wheel_direction = 15,
unk5 = false,
unk6 = v.Cloaked
)
)
v.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
case _ => ()
}
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID
tplayer.ContributionFrom(obj)
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
val bailType = if (tplayer.BailProtection) {
BailType.Bailed
} else {
BailType.Normal
}
sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
)
}
} }

View file

@ -24,9 +24,9 @@ import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, UnuseItemMessa
class SpectatorModeLogic(data: SessionData) extends ModeLogic { class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse) val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse)
val chat: ChatFunctions = ChatLogic(data.chat) val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers) val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general) val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse)
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting) val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad) val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
@ -119,6 +119,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
} }
// //
player.spectator = true player.spectator = true
player.allowInteraction = false
data.chat.JoinChannel(SpectatorChannel) data.chat.JoinChannel(SpectatorChannel)
val newPlayer = SpectatorModeLogic.spectatorCharacter(player) val newPlayer = SpectatorModeLogic.spectatorCharacter(player)
newPlayer.LogActivity(player.History.headOption) newPlayer.LogActivity(player.History.headOption)
@ -152,6 +153,8 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val pguid = player.GUID val pguid = player.GUID
val sendResponse: PlanetSidePacket => Unit = data.sendResponse val sendResponse: PlanetSidePacket => Unit = data.sendResponse
// //
player.spectator = false
player.allowInteraction = true
data.general.stop() data.general.stop()
player.avatar.shortcuts.slice(1, 4) player.avatar.shortcuts.slice(1, 4)
.zipWithIndex .zipWithIndex
@ -159,12 +162,11 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
.map(CreateShortcutMessage(pguid, _, None)) .map(CreateShortcutMessage(pguid, _, None))
.foreach(sendResponse) .foreach(sendResponse)
data.chat.LeaveChannel(SpectatorChannel) data.chat.LeaveChannel(SpectatorChannel)
player.spectator = false
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
zoning.zoneReload = true zoning.zoneReload = true
zoning.spawn.randomRespawn(0.seconds) //to sanctuary zoning.spawn.randomRespawn(10.milliseconds) //to sanctuary
} }
} }
@ -178,7 +180,7 @@ object SpectatorModeLogic {
private def spectatorCharacter(player: Player): Player = { private def spectatorCharacter(player: Player): Player = {
val avatar = player.avatar val avatar = player.avatar
val newAvatar = avatar.copy( val newAvatar = avatar.copy(
basic = avatar.basic.copy(name = "spectator"), basic = avatar.basic,
bep = BattleRank.BR18.experience, bep = BattleRank.BR18.experience,
cep = CommandRank.CR5.experience, cep = CommandRank.CR5.experience,
certifications = Set(), certifications = Set(),
@ -212,6 +214,7 @@ object SpectatorModeLogic {
newPlayer.Position = player.Position newPlayer.Position = player.Position
newPlayer.Orientation = player.Orientation newPlayer.Orientation = player.Orientation
newPlayer.spectator = true newPlayer.spectator = true
newPlayer.bops = true
newPlayer.Spawn() newPlayer.Spawn()
newPlayer newPlayer
} }

View file

@ -1,19 +1,15 @@
// Copyright (c) 2024 PSForever // Copyright (c) 2024 PSForever
package net.psforever.actors.session.spectator package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, ActorRef, typed} import akka.actor.ActorContext
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions}
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} import net.psforever.objects.{Tool, Vehicle, Vehicles}
import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage} import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage}
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} import net.psforever.types.{BailType, PlanetSideGUID, Vector3}
object VehicleHandlerLogic { object VehicleHandlerLogic {
def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = { def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = {
@ -24,9 +20,9 @@ object VehicleHandlerLogic {
class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions { class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
private val galaxyService: ActorRef = ops.galaxyService //private val galaxyService: ActorRef = ops.galaxyService
/** /**
* na * na
@ -151,9 +147,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget => case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget =>
sendResponse(GenericObjectActionMessage(objectGuid, action)) sendResponse(GenericObjectActionMessage(objectGuid, action))
case VehicleResponse.HitHint(sourceGuid) if player.isAlive =>
sendResponse(HitHint(sourceGuid, player.GUID))
case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget => case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget =>
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
val objGuid = obj.GUID val objGuid = obj.GUID
@ -169,14 +162,11 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
//seat number (first field) seems to be correct if passenger is kicked manually by driver //seat number (first field) seems to be correct if passenger is kicked manually by driver
//but always seems to return 4 if user is kicked by mount permissions changing //but always seems to return 4 if user is kicked by mount permissions changing
sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
val typeOfRide = continent.GUID(vehicleGuid) match { continent.GUID(vehicleGuid) match {
case Some(obj: Vehicle) => case Some(obj: Vehicle) =>
sessionLogic.general.unaccessContainer(obj) sessionLogic.general.unaccessContainer(obj)
s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}" case _ => ()
case _ =>
s"${player.Sex.possessive} ride"
} }
log.info(s"${player.Name} has been kicked from $typeOfRide!")
case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) => case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
//seat number (first field) seems to be correct if passenger is kicked manually by driver //seat number (first field) seems to be correct if passenger is kicked manually by driver
@ -194,11 +184,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget => case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget =>
sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid =>
//Only the player that owns this vehicle needs the ownership packet
avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid))
case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget => case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget =>
sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue)) sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue))
@ -211,12 +196,8 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget => case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget =>
sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission)) sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission))
case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget =>
//TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
case VehicleResponse.UnloadVehicle(_, vehicleGuid) => case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1))
case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget =>
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
@ -226,13 +207,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint() sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint()
case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
sessionLogic.zoning.interstellarFerry = Some(vehicle)
sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete)
continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel
log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating")
case VehicleResponse.KickCargo(vehicle, speed, delay) case VehicleResponse.KickCargo(vehicle, speed, delay)
if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 => if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 =>
val strafe = 1 + Vehicles.CargoOrientation(vehicle) val strafe = 1 + Vehicles.CargoOrientation(vehicle)
@ -263,100 +237,9 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive => if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive =>
sessionLogic.vehicles.TotalDriverVehicleControl(cargo) sessionLogic.vehicles.TotalDriverVehicleControl(cargo)
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
if player.VisibleSlots.contains(player.DrawnSlot) =>
player.DrawnSlot = Player.HandsDownSlot
startPlayerSeatedInVehicle(vehicle)
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
startPlayerSeatedInVehicle(vehicle)
case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=true,
lock_wheel=true,
reverse=true,
unk4=false,
lock_vthrust=1,
lock_strafe=0,
movement_speed=0,
unk8=Some(0)
)
)
sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
val vdef = vehicle.Definition
sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=true,
lock_wheel=true,
reverse=false,
unk4=false,
lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
lock_strafe=0,
movement_speed=vdef.AutoPilotSpeed1,
unk8=Some(0)
)
)
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) => case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle)
case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
sendResponse(ChatMsg(
ChatMessageType.CMT_OPEN,
wideContents=true,
recipient="",
s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
note=None
))
case VehicleResponse.PeriodicReminder(_, data) =>
val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
}
sendResponse(ChatMsg(isType, flag, recipient="", msg, None))
case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory)
if player.avatar.vehicle.contains(target) =>
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
continent.GUID(target).collect { case vehicle: Vehicle =>
import net.psforever.login.WorldSession.boolToInt
//owner: must unregister old equipment, and register and install new equipment
(oldWeapons ++ oldInventory).foreach {
case (obj, eguid) =>
sendResponse(ObjectDeleteMessage(eguid, unk1=0))
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
}
sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory)
//jammer or unjamm new weapons based on vehicle status
val vehicleJammered = vehicle.Jammed
addedWeapons
.map { _.obj }
.collect {
case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
jamItem.Jammed = vehicleJammered
JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
}
changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
}
case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _)
if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) =>
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
continent.GUID(target).collect { case vehicle: Vehicle =>
//external participant: observe changes to equipment
(oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
}
case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) => case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) =>
//TODO when vehicle weapons can be changed without visual glitches, rewrite this //TODO when vehicle weapons can be changed without visual glitches, rewrite this
continent.GUID(target).collect { case vehicle: Vehicle => continent.GUID(target).collect { case vehicle: Vehicle =>
@ -384,16 +267,4 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
} }
} }
private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = {
val vehicle_guid = vehicle.GUID
sessionLogic.actionsToCancel()
sessionLogic.terminals.CancelAllProximityUnits()
sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
}
}
} }

View file

@ -1,14 +1,11 @@
// Copyright (c) 2024 PSForever // Copyright (c) 2024 PSForever
package net.psforever.actors.session.spectator package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, typed} import akka.actor.ActorContext
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.Vehicle
import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{DriveState, Vector3} import net.psforever.types.{DriveState, Vector3}
@ -22,213 +19,15 @@ object VehicleLogic {
class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions { class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/* packets */ /* packets */
def handleVehicleState(pkt: VehicleStateMessage): Unit = { def handleVehicleState(pkt: VehicleStateMessage): Unit = { /* can not drive vehicle as spectator */ }
val VehicleStateMessage(
vehicle_guid,
unk1,
pos,
ang,
vel,
is_flying,
unk6,
unk7,
wheels,
is_decelerating,
is_cloaked
) = pkt
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z)
if (obj.MountedIn.isEmpty) {
sessionLogic.updateBlockMap(obj, pos)
}
player.Position = pos //convenient
if (obj.WeaponControlledFromSeat(0).isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = pos
obj.Orientation = ang
if (obj.MountedIn.isEmpty) {
if (obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel
} else {
obj.Velocity = Some(Vector3.Zero)
}
if (obj.Definition.CanFly) {
obj.Flying = is_flying //usually Some(7)
}
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
} else {
obj.Velocity = None
obj.Flying = None
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
player.GUID,
vehicle_guid,
unk1,
obj.Position,
ang,
obj.Velocity,
if (obj.isFlying) {
is_flying
} else {
None
},
unk6,
unk7,
wheels,
is_decelerating,
obj.Cloaked
)
)
sessionLogic.squad.updateSquad()
obj.zoneInteractions()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
)
case _ => ()
}
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { /* can not drive vehicle as spectator */ }
val FrameVehicleStateMessage(
vehicle_guid,
unk1,
pos,
ang,
vel,
unk2,
unk3,
unk4,
is_crouched,
is_airborne,
ascending_flight,
flight_time,
unk9,
unkA
) = pkt
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) =>
sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ =>
(pos, ang, vel, true)
}
player.Position = position //convenient
if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = position
obj.Orientation = angle
obj.Velocity = velocity
// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
// //dev stuff goes here
// }
// else
// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
// //dev stuff goes here
// }
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
if (notMountedState) {
if (obj.DeploymentState != DriveState.Kneeling) {
if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight)
obj.Actor ! BfrFlight.Soaring(flight)
} else if (obj.Flying.nonEmpty) {
obj.Flying = None
obj.Actor ! BfrFlight.Landed
}
} else {
obj.Velocity = None
obj.Flying = None
}
obj.zoneInteractions()
} else {
obj.Velocity = None
obj.Flying = None
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.FrameVehicleState(
player.GUID,
vehicle_guid,
unk1,
position,
angle,
velocity,
unk2,
unk3,
unk4,
is_crouched,
is_airborne,
ascending_flight,
flight_time,
unk9,
unkA
)
)
sessionLogic.squad.updateSquad()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
)
case _ => ()
}
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { /* can not drive vehicle as spectator */ }
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
//is COSM our primary upstream packet?
(o match {
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
case _ => (None, None)
}) match {
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ()
case _ =>
sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals?
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
}
//the majority of the following check retrieves information to determine if we are in control of the child
tools.find { _.GUID == object_guid } match {
case None => ()
case Some(_) => player.Orientation = Vector3(0f, pitch, yaw)
}
if (player.death_by == -1) {
sessionLogic.kickedByAdministration()
}
}
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
@ -261,22 +60,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
} }
} }
def handleDeployRequest(pkt: DeployRequestMessage): Unit = { def handleDeployRequest(pkt: DeployRequestMessage): Unit = { /* can not drive vehicle as spectator */ }
val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
val vehicle = player.avatar.vehicle
if (vehicle.contains(vehicle_guid)) {
if (vehicle == player.VehicleSeated) {
continent.GUID(vehicle_guid) match {
case Some(obj: Vehicle) =>
if (obj.DeploymentState == DriveState.Deployed) {
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
}
case _ => ()
avatarActor ! AvatarActor.SetVehicle(None)
}
}
}
}
/* messages */ /* messages */

View file

@ -3,15 +3,9 @@ package net.psforever.actors.session.spectator
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.equipment.ChargeFireModeDefinition
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, GlobalDefinitions, PlanetSideGameObject, Tool} import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Tool}
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideGUID
object WeaponAndProjectileLogic { object WeaponAndProjectileLogic {
def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
@ -34,18 +28,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ } def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ }
def handleUplinkRequest(packet: UplinkRequest): Unit = { def handleUplinkRequest(packet: UplinkRequest): Unit = { /* intentionally blank */ }
val UplinkRequest(code, _, _) = packet
val playerFaction = player.Faction
//todo this is not correct
code match {
case UplinkRequestType.RevealFriendlies =>
sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction == playerFaction)))
case UplinkRequestType.RevealEnemies =>
sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction != playerFaction)))
case _ => ()
}
}
def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ }
@ -59,9 +42,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
ops.shooting -= item_guid ops.shooting -= item_guid
sessionLogic.findEquipment(item_guid) match { sessionLogic.findEquipment(item_guid) match {
case Some(tool: Tool) if player.VehicleSeated.isEmpty => case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
fireStateStopWhenPlayer(tool, item_guid) ops.fireStateStopWhenPlayer(tool, item_guid)
case Some(tool: Tool) => case Some(tool: Tool) =>
fireStateStopWhenMounted(tool, item_guid) ops.fireStateStopWhenMounted(tool, item_guid)
case Some(trigger: BoomerTrigger) => case Some(trigger: BoomerTrigger) =>
ops.fireStateStopPlayerMessages(item_guid) ops.fireStateStopPlayerMessages(item_guid)
continent.GUID(trigger.Companion).collect { continent.GUID(trigger.Companion).collect {
@ -85,32 +68,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { /* intentionally blank */ } def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { /* intentionally blank */ }
def handleProjectileState(pkt: ProjectileStateMessage): Unit = { def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt ops.handleProjectileState(pkt)
val index = projectile_guid.guid - Projectile.baseUID
ops.projectiles(index) match {
case Some(projectile) if projectile.HasGUID =>
val projectileGlobalUID = projectile.GUID
projectile.Position = shot_pos
projectile.Orientation = shot_orient
projectile.Velocity = shot_vel
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ProjectileState(
player.GUID,
projectileGlobalUID,
shot_pos,
shot_vel,
shot_orient,
seq,
end,
target_guid
)
)
case _ if seq == 0 =>
/* missing the first packet in the sequence is permissible */
case _ =>
log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
}
} }
def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { /* intentionally blank */ } def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { /* intentionally blank */ }
@ -122,97 +80,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ } def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ }
def handleAIDamage(pkt: AIDamage): Unit = { /* intentionally blank */ } def handleAIDamage(pkt: AIDamage): Unit = { /* intentionally blank */ }
/* support code */
/**
* After a weapon has finished shooting, determine if it needs to be sorted in a special way.
* @param tool a weapon
*/
private def FireCycleCleanup(tool: Tool): Unit = {
//TODO replaced by more appropriate functionality in the future
val tdef = tool.Definition
if (GlobalDefinitions.isGrenade(tdef)) {
val ammoType = tool.AmmoType
FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters
case Nil =>
log.info(s"${player.Name} has no more $ammoType grenades to throw")
RemoveOldEquipmentFromInventory(player)(tool)
case x :: xs => //this is similar to ReloadMessage
val box = x.obj.asInstanceOf[Tool]
val tailReloadValue: Int = if (xs.isEmpty) { 0 }
else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum }
val sumReloadValue: Int = box.Magazine + tailReloadValue
val actualReloadValue = if (sumReloadValue <= 3) {
RemoveOldEquipmentFromInventory(player)(x.obj)
sumReloadValue
} else {
ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
3
}
log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw")
ModifyAmmunition(player)(
tool.AmmoSlot.Box,
-actualReloadValue
) //grenade item already in holster (negative because empty)
xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) })
}
} else if (tdef == GlobalDefinitions.phoenix) {
RemoveOldEquipmentFromInventory(player)(tool)
}
}
/**
* Given an object that contains a box of amunition in its `Inventory` at a certain location,
* change the amount of ammunition within that box.
* @param obj the `Container`
* @param box an `AmmoBox` to modify
* @param reloadValue the value to modify the `AmmoBox`;
* subtracted from the current `Capacity` of `Box`
*/
private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
val capacity = box.Capacity - reloadValue
box.Capacity = capacity
sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
}
private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = {
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ChangeFireState_Start(player.GUID, itemGuid)
)
}
/*
used by ChangeFireStateMessage_Stop handling
*/
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
tool.FireMode match {
case _: ChargeFireModeDefinition =>
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
case _ => ()
}
if (tool.Magazine == 0) {
FireCycleCleanup(tool)
}
}
private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = {
//the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why
//suppress the decimator's alternate fire mode, however
if (
tool.Definition == GlobalDefinitions.phoenix &&
tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile
) {
fireStateStartPlayerMessages(itemGuid)
}
fireStateStopUpdateChargeAndCleanup(tool)
ops.fireStateStopPlayerMessages(itemGuid)
}
private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = {
fireStateStopUpdateChargeAndCleanup(tool)
ops.fireStateStopMountedMessages(itemGuid)
}
} }

View file

@ -7,9 +7,8 @@ import akka.actor.typed.ActorRef
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.session.normal.{NormalMode => SessionNormalMode}
import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectatorMode}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.LivePlayerList import net.psforever.objects.LivePlayerList
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
@ -21,6 +20,7 @@ import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServic
import net.psforever.types.ChatMessageType.CMT_QUIT import net.psforever.types.ChatMessageType.CMT_QUIT
import org.log4s.Logger import org.log4s.Logger
import java.util.concurrent.{Executors, TimeUnit}
import scala.annotation.unused import scala.annotation.unused
import scala.collection.{Seq, mutable} import scala.collection.{Seq, mutable}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -67,6 +67,8 @@ class ChatOperations(
) extends CommonSessionInterfacingFunctionality { ) extends CommonSessionInterfacingFunctionality {
private var channels: List[ChatChannel] = List() private var channels: List[ChatChannel] = List()
private var silenceTimer: Cancellable = Default.Cancellable private var silenceTimer: Cancellable = Default.Cancellable
private[session] var transitoryCommandEntered: Option[ChatMessageType] = None
private val scheduler = Executors.newScheduledThreadPool(2)
/** /**
* when another player is listed as one of our ignored players, * when another player is listed as one of our ignored players,
* and that other player sends an emote, * and that other player sends an emote,
@ -75,6 +77,8 @@ class ChatOperations(
*/ */
private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]() private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
private[session] var CurrentSpectatorMode: PlayerMode = SpectatorMode
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse] private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse]
@ -124,17 +128,6 @@ class ChatOperations(
sendResponse(message.copy(contents = f"$speed%.3f")) sendResponse(message.copy(contents = f"$speed%.3f"))
} }
def commandToggleSpectatorMode(session: Session, contents: String): Unit = {
val currentSpectatorActivation = session.player.spectator
contents.toLowerCase() match {
case "on" | "o" | "" if !currentSpectatorActivation =>
context.self ! SessionActor.SetMode(SessionSpectatorMode)
case "off" | "of" if currentSpectatorActivation =>
context.self ! SessionActor.SetMode(SessionNormalMode)
case _ => ()
}
}
def commandRecall(session: Session): Unit = { def commandRecall(session: Session): Unit = {
val player = session.player val player = session.player
val errorMessage = session.zoningType match { val errorMessage = session.zoningType match {
@ -349,10 +342,49 @@ class ChatOperations(
} }
} }
//evaluate results //evaluate results
if (!commandCaptureBaseProcessResults(resolvedFacilities, resolvedFaction, resolvedTimer)) {
if (usageMessage) {
sendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
)
} else {
val msg = if (facilityError == 1) { "can not contextually determine building target" }
else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
else { "malformed params; check usage" }
sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
}
}
}
def commandCaptureBaseProcessResults(
resolvedFacilities: Option[Seq[Building]],
resolvedFaction: Option[PlanetSideEmpire.Value],
resolvedTimer: Option[Int]
): Boolean = {
//evaluate results
(resolvedFacilities, resolvedFaction, resolvedTimer) match { (resolvedFacilities, resolvedFaction, resolvedTimer) match {
case (Some(buildings), Some(faction), Some(_)) => case (Some(buildings), Some(faction), Some(_)) =>
buildings.foreach { building =>
//TODO implement timer //TODO implement timer
//schedule processing of buildings with a delay
processBuildingsWithDelay(buildings, faction, 1000) //delay of 1000ms between each building operation
true
case _ =>
false
}
}
def processBuildingsWithDelay(
buildings: Seq[Building],
faction: PlanetSideEmpire.Value,
delayMillis: Long
): Unit = {
val buildingIterator = buildings.iterator
scheduler.scheduleAtFixedRate(
() => {
if (buildingIterator.hasNext) {
val building = buildingIterator.next()
val terminal = building.CaptureTerminal.get val terminal = building.CaptureTerminal.get
val zone = building.Zone val zone = building.Zone
val zoneActor = zone.actor val zoneActor = zone.actor
@ -372,20 +404,11 @@ class ChatOperations(
//push for map updates again //push for map updates again
zoneActor ! ZoneActor.ZoneMapUpdate() zoneActor ! ZoneActor.ZoneMapUpdate()
} }
case _ => },
if (usageMessage) { 0,
sendResponse( delayMillis,
message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") TimeUnit.MILLISECONDS
) )
} else {
val msg = if (facilityError == 1) { "can not contextually determine building target" }
else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
else { "malformed params; check usage" }
sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
}
}
} }
def commandVoice(session: Session, message: ChatMsg, contents: String, toChannel: ChatChannel): Unit = { def commandVoice(session: Session, message: ChatMsg, contents: String, toChannel: ChatChannel): Unit = {
@ -615,8 +638,9 @@ class ChatOperations(
} }
def commandAddCertification(session: Session, message: ChatMsg, contents: String): Unit = { def commandAddCertification(session: Session, message: ChatMsg, contents: String): Unit = {
val certs = cliTokenization(contents).map(name => Certification.values.find(_.name == name)) val tokens = cliTokenization(contents)
val result = if (certs.nonEmpty) { val certs = tokens.map(name => Certification.values.find(_.name == name))
val result = if (tokens.nonEmpty) {
if (certs.contains(None)) { if (certs.contains(None)) {
s"@AckErrorCertifications" s"@AckErrorCertifications"
} else { } else {
@ -635,7 +659,7 @@ class ChatOperations(
} }
def commandKick(session: Session, message: ChatMsg, contents: String): Unit = { def commandKick(session: Session, message: ChatMsg, contents: String): Unit = {
val inputs = cliTokenization(contents) val inputs = cliTokenizationCaseSensitive(contents)
inputs.headOption match { inputs.headOption match {
case Some(input) => case Some(input) =>
val determination: Player => Boolean = input.toLongOption match { val determination: Player => Boolean = input.toLongOption match {
@ -1009,13 +1033,20 @@ class ChatOperations(
private def captureBaseCurrSoi( private def captureBaseCurrSoi(
session: Session session: Session
): Iterable[Building] = { ): Iterable[Building] = {
val charId = session.player.CharId val player = session.player
session.zone.Buildings.values.filter { building => val positionxy = player.Position.xy
building.PlayersInSOI.exists(_.CharId == charId) session
.zone
.blockMap
.sector(player)
.buildingList
.filter { building =>
val radius = building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius
} }
} }
private def captureBaseParamFaction( def captureBaseParamFaction(
@unused session: Session, @unused session: Session,
token: Option[String] token: Option[String]
): Option[PlanetSideEmpire.Value] = { ): Option[PlanetSideEmpire.Value] = {
@ -1238,7 +1269,7 @@ class ChatOperations(
params: Seq[String] params: Seq[String]
): Boolean = { ): Boolean = {
val ourRank = BattleRank.withExperience(session.avatar.bep).value val ourRank = BattleRank.withExperience(session.avatar.bep).value
if (!session.account.gm && if (!avatar.permissions.canGM &&
(ourRank <= Config.app.game.promotion.broadcastBattleRank || (ourRank <= Config.app.game.promotion.broadcastBattleRank ||
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) { ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
setBattleRank(session, params, AvatarActor.Progress) setBattleRank(session, params, AvatarActor.Progress)
@ -1322,7 +1353,33 @@ class ChatOperations(
} }
def cliTokenization(str: String): List[String] = { def cliTokenization(str: String): List[String] = {
str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList.filter(!_.equals(""))
}
def cliTokenizationCaseSensitive(str: String): List[String] = {
str.replaceAll("\\s+", " ").trim.split("\\s").toList.filter(!_.equals(""))
}
def cliCommaSeparatedParams(params: Seq[String]): Seq[String] = {
var len = 0
var appendNext = false
var formattedParams: Seq[String] = Seq()
params.foreach {
case "," =>
appendNext = true
case param if appendNext || param.startsWith(",") =>
formattedParams = formattedParams.slice(0, len - 1) :+ formattedParams(len - 1) + "," + param.replaceAll(",", "")
appendNext = param.endsWith(",")
case param if param.endsWith(",") =>
formattedParams = formattedParams :+ param.take(param.length-1)
len += 1
appendNext = true
case param =>
formattedParams = formattedParams :+ param
len += 1
appendNext = false
}
formattedParams
} }
def commandIncomingSend(message: ChatMsg): Unit = { def commandIncomingSend(message: ChatMsg): Unit = {

View file

@ -3,11 +3,27 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, Terminal}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, VehicleSource}
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.objects.zones.exp.ToDatabase
import net.psforever.services.RemoverActor
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Future, Promise}
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Success
// //
import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.login.WorldSession._ import net.psforever.login.WorldSession._
@ -170,6 +186,217 @@ class GeneralOperations(
private[session] var progressBarUpdate: Cancellable = Default.Cancellable private[session] var progressBarUpdate: Cancellable = Default.Cancellable
private var charSavedTimer: Cancellable = Default.Cancellable private var charSavedTimer: Cancellable = Default.Cancellable
def handleDropItem(pkt: DropItemMessage): GeneralOperations.ItemDropState.Behavior = {
val DropItemMessage(itemGuid) = pkt
(sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match {
case (Some(anItem: Equipment), Some(heldItem))
if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty =>
RemoveOldEquipmentFromInventory(player)(heldItem)
GeneralOperations.ItemDropState.Dropped
case (Some(anItem: Equipment), Some(heldItem))
if anItem eq heldItem =>
DropEquipmentFromInventory(player)(heldItem)
GeneralOperations.ItemDropState.Dropped
case (Some(_), _) =>
GeneralOperations.ItemDropState.NotDropped
case _ =>
GeneralOperations.ItemDropState.NotFound
}
}
def handlePickupItem(pkt: PickupItemMessage): GeneralOperations.ItemPickupState.Behavior = {
val PickupItemMessage(itemGuid, _, _, _) = pkt
sessionLogic.validObject(itemGuid, decorator = "PickupItem") match {
case Some(item: Equipment)
if player.Fit(item).nonEmpty =>
PickUpEquipmentFromGround(player)(item)
GeneralOperations.ItemPickupState.PickedUp
case Some(_: Equipment) =>
GeneralOperations.ItemPickupState.Dropped
case _ =>
GeneralOperations.ItemPickupState.NotFound
}
}
def handleZipLine(pkt: ZipLineMessage): GeneralOperations.ZiplineBehavior.Behavior = {
val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt
continent.zipLinePaths.find(x => x.PathId == pathId) match {
case Some(path) if path.IsTeleporter =>
val endPoint = path.ZipLinePoints.last
sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
//todo: send to zone to show teleport animation to all clients
sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
GeneralOperations.ZiplineBehavior.Teleporter
case Some(_) =>
//todo: send to zone to show zipline animation to all clients
action match {
case 0 =>
//travel along the zipline in the direction specified
sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
GeneralOperations.ZiplineBehavior.Zipline
case 1 =>
//disembark from zipline at destination
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
GeneralOperations.ZiplineBehavior.Zipline
case 2 =>
//get off by force
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
GeneralOperations.ZiplineBehavior.Zipline
case _ =>
GeneralOperations.ZiplineBehavior.Unsupported
}
case _ =>
GeneralOperations.ZiplineBehavior.NotFound
}
}
def handleMoveItem(pkt: MoveItemMessage): Unit = {
val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt
(
continent.GUID(sourceGuid),
continent.GUID(destinationGuid),
sessionLogic.validObject(itemGuid, decorator = "MoveItem")
) match {
case (
Some(source: PlanetSideServerObject with Container),
Some(destination: PlanetSideServerObject with Container),
Some(item: Equipment)
) =>
ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest))
case (None, _, _) =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object"
)
case (_, None, _) =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object"
)
case (_, _, None) => ()
case _ =>
log.error(
s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered"
)
}
}
def handleLootItem(pkt: LootItemMessage): Unit = {
val LootItemMessage(itemGuid, targetGuid) = pkt
(sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match {
case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) =>
//figure out the source
(
{
val findFunc: PlanetSideServerObject with Container => Option[
(PlanetSideServerObject with Container, Option[Int])
] = findInLocalContainer(itemGuid)
findFunc(player.avatar.locker)
.orElse(findFunc(player))
.orElse(accessedContainer match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
})
},
destination.Fit(item)
) match {
case (Some((source, Some(_))), Some(dest)) =>
ContainableMoveItem(player.Name, source, destination, item, dest)
case (None, _) =>
log.error(s"LootItem: ${player.Name} can not find where $item is put currently")
case (_, None) =>
log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination")
case _ =>
log.error(
s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered"
)
}
case (Some(obj), _) =>
log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}")
case (None, _) => ()
case (_, None) =>
log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid")
}
}
def handleAvatarImplant(pkt: AvatarImplantMessage): GeneralOperations.ImplantActivationBehavior.Behavior = {
val AvatarImplantMessage(_, action, slot, status) = pkt
if (action == ImplantAction.Activation) {
if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
//do not activate; play deactivation sound instead
sessionLogic.zoning.spawn.stopDeconstructing()
avatar.implants(slot).collect {
case implant if implant.active =>
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
case implant =>
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
}
GeneralOperations.ImplantActivationBehavior.Failed
} else {
avatar.implants(slot) match {
case Some(implant) =>
if (status == 1) {
avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType)
GeneralOperations.ImplantActivationBehavior.Activate
} else {
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
GeneralOperations.ImplantActivationBehavior.Deactivate
}
case _ =>
GeneralOperations.ImplantActivationBehavior.NotFound
}
}
} else {
GeneralOperations.ImplantActivationBehavior.Failed
}
}
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
val CreateShortcutMessage(_, slot, shortcutOpt) = pkt
shortcutOpt match {
case Some(shortcut) =>
avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
case None =>
avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
}
}
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
val ObjectDetectedMessage(_, _, _, targets) = pkt
sessionLogic.shooting.FindWeapon.foreach {
case weapon if weapon.Projectile.AutoLock =>
//projectile with auto-lock instigates a warning on the target
val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets)
val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0)
detectedTargets.foreach { target =>
continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode))
}
case _ => ()
}
}
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
val TargetingImplantRequest(list) = pkt
val targetInfo: List[TargetInfo] = list.flatMap { x =>
continent.GUID(x.target_guid) match {
case Some(player: Player) =>
val health = player.Health.toFloat / player.MaxHealth
val armor = if (player.MaxArmor > 0) {
player.Armor.toFloat / player.MaxArmor
} else {
0
}
Some(TargetInfo(player.GUID, health, armor))
case _ =>
log.warn(
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
)
None
}
}
sendResponse(TargetingInfoMessage(targetInfo))
}
/** /**
* Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays.
* Intended to assist in sanitizing loadout information from the perspective of the player, or target owner. * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner.
@ -218,6 +445,13 @@ class GeneralOperations(
attacker.Name, attacker.Name,
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit) AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
) )
ToDatabase.reportFacilityCapture(
attacker.CharId,
continent.Number,
llu.Owner.GUID.guid,
Config.app.game.experience.cep.lluSlayerCredit,
expType = "lsc" //three characters - "llu slayer credit"
)
} }
} }
if (!CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player)) { if (!CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player)) {
@ -560,6 +794,66 @@ class GeneralOperations(
parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) } parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) }
} }
/**
* A simple object searching algorithm that is limited to containers currently known and accessible by the player.
* If all relatively local containers are checked and the object is not found,
* the player's locker inventory will be checked, and then
* the game environment (items on the ground) will be checked too.
* If the target object is discovered, it is removed from its current location and is completely destroyed.
* @see `RequestDestroyMessage`
* @see `Zone.ItemIs.Where`
* @param objectGuid the target object's globally unique identifier;
* it is not expected that the object will be unregistered, but it is also not gauranteed
* @param obj the target object
* @return `true`, if the target object was discovered and removed;
* `false`, otherwise
*/
def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = {
val findFunc
: PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
findInLocalContainer(objectGuid)
findFunc(player)
.orElse(accessedContainer match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
})
.orElse(sessionLogic.vehicles.findLocalVehicle match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
case _ =>
None
}) match {
case Some((parent, Some(_))) =>
obj.Position = Vector3.Zero
RemoveOldEquipmentFromInventory(parent)(obj)
true
case _ if player.avatar.locker.Inventory.Remove(objectGuid) =>
sendResponse(ObjectDeleteMessage(objectGuid, 0))
true
case _ if continent.EquipmentOnGround.contains(obj) =>
obj.Position = Vector3.Zero
continent.Ground ! Zone.Ground.RemoveItem(objectGuid)
continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
true
case _ =>
Zone.EquipmentIs.Where(obj, objectGuid, continent) match {
case None =>
true
case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID =>
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
true
case Some(Zone.EquipmentIs.Orphaned()) =>
true
case _ =>
log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it")
false
}
}
}
/** /**
* na * na
* @param targetGuid na * @param targetGuid na
@ -692,10 +986,10 @@ class GeneralOperations(
) )
} }
def administrativeKick(tplayer: Player): Unit = { def administrativeKick(tplayer: Player, time: Option[Long]): Unit = {
log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
tplayer.death_by = -1 tplayer.death_by = -1
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name, time)
//get out of that vehicle //get out of that vehicle
sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match { sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match {
case (Some(obj), Some(seatNum)) => case (Some(obj), Some(seatNum)) =>
@ -767,6 +1061,460 @@ class GeneralOperations(
) )
} }
def handleDeployObject(
zone: Zone,
deployableType: DeployedItem.Value,
position: Vector3,
orientation: Vector3,
side: Sidedness,
faction: PlanetSideEmpire.Value,
owner: Player,
builtWith: ConstructionItem
): Future[Deployable] = {
val (deployableEntity, tasking) = commonHandleDeployObjectSetup(zone, deployableType, position, orientation, side, faction)
deployableEntity.AssignOwnership(owner)
val promisedDeployable: Promise[Deployable] = Promise()
//execute
val result = TaskWorkflow.execute(CallBackForTask(
tasking,
zone.Deployables,
Zone.Deployable.BuildByOwner(deployableEntity, owner, builtWith),
context.self
))
result.onComplete {
case Success(_) => promisedDeployable.success(deployableEntity)
case _ => ()
}
promisedDeployable.future
}
def handleDeployObject(
zone: Zone,
deployableType: DeployedItem.Value,
position: Vector3,
orientation: Vector3,
side: Sidedness,
faction: PlanetSideEmpire.Value
): Future[Deployable] = {
val (deployableEntity, tasking) = commonHandleDeployObjectSetup(zone, deployableType, position, orientation, side, faction)
val promisedDeployable: Promise[Deployable] = Promise()
//execute
val result = TaskWorkflow.execute(CallBackForTask(
tasking,
zone.Deployables,
Zone.Deployable.Build(deployableEntity),
context.self
))
result.onComplete {
case Success(_) =>
Players.buildCooldownReset(zone, player.Name, deployableEntity.GUID)
deployableEntity.Actor ! Deployable.Deconstruct(Some(20.minutes))
if (deployableType == DeployedItem.boomer) {
val trigger = new BoomerTrigger
trigger.Companion = deployableEntity.GUID
deployableEntity.asInstanceOf[BoomerDeployable].Trigger = trigger
TaskWorkflow.execute(CallBackForTask(
GUIDTask.registerEquipment(zone.GUID, trigger),
zone.Ground,
Zone.Ground.DropItem(trigger, position + Vector3.z(value = 0.5f), Vector3.z(orientation.z))
))
}
promisedDeployable.success(deployableEntity)
case _ => ()
}
promisedDeployable.future
}
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)
}
}
def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
val vehicleOpt = continent.GUID(player.avatar.vehicle)
(vehicleOpt, equipment) match {
case (Some(vehicle: Vehicle), Some(item))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle))
case (Some(vehicle: Vehicle), _)
if vehicle.Definition == GlobalDefinitions.ant &&
vehicle.DeploymentState == DriveState.Deployed &&
Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) =>
resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle))
case _ => ()
}
}
def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
if (obj.isBackpack) {
if (equipment.isEmpty) {
log.info(s"${player.Name} is looting the corpse of ${obj.Name}")
sendResponse(msg)
accessContainer(obj)
}
} else if (!msg.unk3 && player.isAlive) { //potential kit use
(continent.GUID(msg.item_used_guid), kitToBeUsed) match {
case (Some(kit: Kit), None) =>
kitToBeUsed = Some(msg.item_used_guid)
player.Actor ! CommonMessages.Use(player, Some(kit))
case (Some(_: Kit), Some(_)) | (None, Some(_)) =>
//a kit is already queued to be used; ignore this request
sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None))
case (Some(item), _) =>
log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead")
case (None, None) =>
log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") }
} else if (msg.object_id == ObjectClass.avatar && msg.unk3) {
equipment match {
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank =>
obj.Actor ! CommonMessages.Use(player, equipment)
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
obj.Actor ! CommonMessages.Use(player, equipment)
case _ => ()
}
}
}
def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(locker, item)
case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty =>
log.info(s"${player.Name} is accessing a locker")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
val playerLocker = player.avatar.locker
sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456))
accessContainer(playerLocker)
case _ => ()
}
}
def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(captureTerminal, item)
case _ if specialItemSlotGuid.nonEmpty =>
continent.GUID(specialItemSlotGuid) match {
case Some(llu: CaptureFlag) =>
if (llu.Target.GUID == captureTerminal.Owner.GUID) {
continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu))
} else {
log.info(
s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}"
)
}
case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU")
}
case _ => ()
}
}
def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment.foreach { item =>
sendUseGeneralEntityMessage(obj, item)
obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path
}
}
def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(obj, item)
case None if player.Faction == obj.Faction =>
//access to trunk
if (
obj.AccessingTrunk.isEmpty &&
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
.contains(player.GUID))
) {
log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.AccessingTrunk = player.GUID
accessContainer(obj)
sendResponse(msg)
}
case _ => ()
}
}
def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(terminal, item)
case None
if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction ||
terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL =>
val tdef = terminal.Definition
if (tdef.isInstanceOf[MatrixTerminalDefinition]) {
//TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sendResponse(
BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position)
)
} else if (
tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal ||
tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal
) {
sessionLogic.vehicles.findLocalVehicle match {
case Some(vehicle) =>
log.info(
s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}"
)
sendResponse(msg)
sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId))
case None =>
log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none")
}
} else if (tdef == GlobalDefinitions.teleportpad_terminal) {
//explicit request
log.info(s"${player.Name} is purchasing a router telepad")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
)
} else if (tdef == GlobalDefinitions.targeting_laser_dispenser) {
//explicit request
log.info(s"${player.Name} is purchasing a targeting laser")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0))
)
} else {
log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sendResponse(msg)
}
case _ => ()
}
}
def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = {
equipment match {
case Some(item) =>
sendUseGeneralEntityMessage(obj, item)
case None if player.Faction == obj.Faction =>
//deconstruction
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
sessionLogic.actionsToCancel()
sessionLogic.terminals.CancelAllProximityUnits()
sessionLogic.zoning.spawn.startDeconstructing(obj)
case _ => ()
}
}
def handleUseTelepadDeployable(
obj: TelepadDeployable,
equipment: Option[Equipment],
msg: UseItemMessage,
useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit
): Unit = {
if (equipment.isEmpty) {
(continent.GUID(obj.Router) match {
case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
case Some(vehicle) => Some(vehicle, None)
case None => None
}) match {
case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
player.WhichSide = vehicle.WhichSide
useTelepadFunc(vehicle, util, obj, obj, util)
case Some((vehicle: Vehicle, None)) =>
log.error(
s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
)
case Some((o, _)) =>
log.error(
s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}"
)
obj.Actor ! Deployable.Deconstruct()
case _ => ()
}
}
}
def handleUseInternalTelepad(
obj: InternalTelepad,
msg: UseItemMessage,
useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit
): Unit = {
continent.GUID(obj.Telepad) match {
case Some(pad: TelepadDeployable) =>
player.WhichSide = pad.WhichSide
useTelepadFunc(obj.Owner.asInstanceOf[Vehicle], obj, pad, obj, pad)
case Some(o) =>
log.error(
s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}"
)
case None => ()
}
}
/**
* A player uses a fully-linked Router teleportation system.
* @param router the Router vehicle
* @param internalTelepad the internal telepad within the Router vehicle
* @param remoteTelepad the remote telepad that is currently associated with this Router
* @param src the origin of the teleportation (where the player starts)
* @param dest the destination of the teleportation (where the player is going)
*/
def useRouterTelepadSystem(
router: Vehicle,
internalTelepad: InternalTelepad,
remoteTelepad: TelepadDeployable,
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
val time = System.currentTimeMillis()
if (
time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
val pguid = player.GUID
val sguid = src.GUID
val dguid = dest.GUID
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
useRouterTelepadEffect(pguid, sguid, dguid)
continent.LocalEvents ! LocalServiceMessage(
continent.id,
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
)
player.Position = dest.Position
player.LogActivity(TelepadUseActivity(VehicleSource(router), DeployableSource(remoteTelepad), PlayerSource(player)))
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}
recentTeleportAttempt = time
}
/**
* A player uses a fully-linked Router teleportation system.
* @param router the Router vehicle
* @param internalTelepad the internal telepad within the Router vehicle
* @param remoteTelepad the remote telepad that is currently associated with this Router
* @param src the origin of the teleportation (where the player starts)
* @param dest the destination of the teleportation (where the player is going)
*/
def useRouterTelepadSystemSecretly(
router: Vehicle,
internalTelepad: InternalTelepad,
remoteTelepad: TelepadDeployable,
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
val time = System.currentTimeMillis()
if (
time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
val pguid = player.GUID
val sguid = src.GUID
val dguid = dest.GUID
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
useRouterTelepadEffect(pguid, sguid, dguid)
player.Position = dest.Position
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}
recentTeleportAttempt = time
}
def handleUseCaptureFlag(obj: CaptureFlag): Unit = {
// LLU can normally only be picked up the faction that owns it
specialItemSlotGuid match {
case None if obj.Faction == player.Faction =>
specialItemSlotGuid = Some(obj.GUID)
player.Carrying = SpecialCarry.CaptureFlag
continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player)
case None =>
log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}")
case Some(guid) if guid != obj.GUID =>
// Ignore duplicate pickup requests
log.warn(
s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid"
)
case _ => ()
}
}
def handleUseWarpGate(equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
(continent.GUID(player.VehicleSeated), equipment) match {
case (Some(vehicle: Vehicle), Some(item))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
vehicle.Actor ! CommonMessages.Use(player, equipment)
case _ => ()
}
}
def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = {
equipment.foreach { item =>
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.Actor ! CommonMessages.Use(player, Some(item))
}
}
def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.Actor ! CommonMessages.Use(player, Some(equipment))
}
def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
equipment match {
case Some(item)
if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) ||
GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => ()
case _ =>
log.warn(s"UseItem: ${player.Name} does not know how to handle $obj")
}
}
def commonFacilityShieldCharging(obj: PlanetSideServerObject with BlockMapEntity): Unit = {
obj.Actor ! CommonMessages.ChargeShields(
15,
Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius))
)
}
private def commonHandleDeployObjectSetup(
zone: Zone,
deployableType: DeployedItem.Value,
position: Vector3,
orientation: Vector3,
side: Sidedness,
faction: PlanetSideEmpire.Value
): (Deployable, TaskBundle) = {
val deployableEntity: Deployable = Deployables.Make(deployableType)()
deployableEntity.Position = position
deployableEntity.Orientation = orientation
deployableEntity.WhichSide = side
deployableEntity.Faction = faction
val tasking: TaskBundle = deployableEntity match {
case turret: TurretDeployable =>
GUIDTask.registerDeployableTurret(zone.GUID, turret)
case _ =>
GUIDTask.registerObject(zone.GUID, deployableEntity)
}
(deployableEntity, tasking)
}
override protected[session] def actionsToCancel(): Unit = { override protected[session] def actionsToCancel(): Unit = {
progressBarValue = None progressBarValue = None
kitToBeUsed = None kitToBeUsed = None
@ -786,11 +1534,12 @@ class GeneralOperations(
} }
} }
case Some(o) if player.isAlive =>
unaccessContainer(o)
sendResponse(UnuseItemMessage(player.GUID, o.GUID))
case Some(o) => case Some(o) =>
unaccessContainer(o) unaccessContainer(o)
if (player.isAlive) {
sendResponse(UnuseItemMessage(player.GUID, o.GUID))
}
case None => () case None => ()
} }
@ -801,3 +1550,42 @@ class GeneralOperations(
charSavedTimer.cancel() charSavedTimer.cancel()
} }
} }
object GeneralOperations {
object UseItem {
sealed trait Behavior
case object Handled extends Behavior
case object HandledPassive extends Behavior
case object Unhandled extends Behavior
}
object ItemDropState {
sealed trait Behavior
case object Dropped extends Behavior
case object NotDropped extends Behavior
case object NotFound extends Behavior
}
object ItemPickupState {
sealed trait Behavior
case object PickedUp extends Behavior
case object Dropped extends Behavior
case object NotFound extends Behavior
}
object ZiplineBehavior {
sealed trait Behavior
case object Teleporter extends Behavior
case object Zipline extends Behavior
case object Unsupported extends Behavior
case object NotFound extends Behavior
}
object ImplantActivationBehavior {
sealed trait Behavior
case object Activate extends Behavior
case object Deactivate extends Behavior
case object Failed extends Behavior
case object NotFound extends Behavior
}
}

View file

@ -2,9 +2,13 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.{Default, PlanetSideGameObject, Player}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.objects.zones.exp import net.psforever.objects.zones.exp
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse}
import scala.collection.mutable import scala.collection.mutable
// //
@ -55,7 +59,7 @@ class SessionAvatarHandlers(
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023 //TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
//must be in a squad to earn experience //must be in a squad to earn experience
val charId = player.CharId val charId = player.CharId
val squadUI = sessionLogic.squad.squadUI /*val squadUI = sessionLogic.squad.squadUI
val participation = continent val participation = continent
.Building(buildingId) .Building(buildingId)
.map { building => .map { building =>
@ -113,6 +117,34 @@ class SessionAvatarHandlers(
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep") exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp) avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp)
Some(modifiedExp) Some(modifiedExp)
}*/
//if not in squad (temporary)
exp.ToDatabase.reportFacilityCapture(charId, zoneNumber, buildingId, cep, expType="bep")
avatarActor ! AvatarActor.AwardFacilityCaptureBep(cep)
}
/**
*
* @param killer the player who got the kill
* @param exp the amount of bep they received for the kill
* Squad members of a "killer" will receive a split of the experience if they are both alive
* and in the same zone as the killer. The amount received is
* based on the size of the squad. Each squad member that meets the criteria will receive a fractional split.
*/
def shareKillExperienceWithSquad(killer: Player, exp: Long): Unit = {
//TODO consider squad experience waypoint in exp calculation
val squadUI = sessionLogic.squad.squadUI
val squadSize = squadUI.size
if (squadSize > 1) {
val expSplit = exp / squadSize
val squadMembers = squadUI.filterNot(_._1 == killer.CharId).map { case (_, member) => member }.toList.map(_.name)
val playersInZone = killer.Zone.Players.map { avatar => (avatar.id, avatar.basic.name) }
val squadMembersHere = playersInZone.filter(member => squadMembers.contains(member._2))
squadMembersHere.foreach { member =>
killer.Zone.AvatarEvents ! AvatarServiceMessage(
member._2,
AvatarAction.AwardBep(member._1, expSplit, ExperienceType.Normal))
}
} }
} }
@ -154,6 +186,45 @@ class SessionAvatarHandlers(
victimSeated victimSeated
) )
} }
def revive(revivalTargetGuid: PlanetSideGUID): Unit = {
val spawn = sessionLogic.zoning.spawn
spawn.reviveTimer.cancel()
spawn.reviveTimer = Default.Cancellable
spawn.respawnTimer.cancel()
spawn.respawnTimer = Default.Cancellable
player.Revive
val health = player.Health
sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
)
}
def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = {
val playerName = player.Name
//boot cadaver from mount on client
context.self ! AvatarServiceResponse(
playerName,
Service.defaultPlayerGUID,
AvatarResponse.SendResponse(
ObjectDetachMessage(obj.GUID, playerGuid, player.Position, Vector3.Zero)
)
)
//player no longer seated
obj.PassengerInSeat(player).foreach { seatNumber =>
//boot cadaver from mount internally (vehicle perspective)
obj.Seats(seatNumber).unmount(player)
//inform client-specific logic
context.self ! Mountable.MountMessages(
player,
Mountable.CanDismount(obj, seatNumber, 0)
)
}
player.VehicleSeated = None
}
} }
object SessionAvatarHandlers { object SessionAvatarHandlers {

View file

@ -89,6 +89,7 @@ class SessionData(
private[session] var persistFunc: () => Unit = noPersistence private[session] var persistFunc: () => Unit = noPersistence
private[session] var persist: () => Unit = updatePersistenceOnly private[session] var persist: () => Unit = updatePersistenceOnly
private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial
private[session] var keepAlivePersistenceFunc: () => Unit = keepAlivePersistence
private[session] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet private[session] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet
private[session] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() private[session] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]()
private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0) private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0)
@ -478,7 +479,7 @@ class SessionData(
def keepAlivePersistenceInitial(): Unit = { def keepAlivePersistenceInitial(): Unit = {
persist() persist()
if (player != null && player.HasGUID) { if (player != null && player.HasGUID) {
keepAliveFunc = keepAlivePersistence keepAliveFunc = keepAlivePersistenceFunc
} }
} }

View file

@ -6,9 +6,9 @@ import net.psforever.objects.{Players, TurretDeployable}
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.interior.Sidedness import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.packet.game.GenericObjectActionMessage import net.psforever.packet.game.{GenericObjectActionMessage, ObjectDeleteMessage, PlanetsideAttributeMessage, TriggerEffectMessage}
import net.psforever.services.local.LocalResponse import net.psforever.services.local.LocalResponse
import net.psforever.types.PlanetSideGUID import net.psforever.types.{PlanetSideGUID, Vector3}
trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality { trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality {
def ops: SessionLocalHandlers def ops: SessionLocalHandlers
@ -30,12 +30,12 @@ class SessionLocalHandlers(
} }
def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = { def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = {
Players.buildCooldownReset(continent, player.Name, obj) Players.buildCooldownReset(continent, player.Name, obj.GUID)
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj)) TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj))
} }
def handleDeployableIsDismissed(obj: Deployable): Unit = { def handleDeployableIsDismissed(obj: Deployable): Unit = {
Players.buildCooldownReset(continent, player.Name, obj) Players.buildCooldownReset(continent, player.Name, obj.GUID)
TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj)) TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj))
} }
@ -47,4 +47,26 @@ class SessionLocalHandlers(
else else
400f 400f
} }
/**
* Common behavior for deconstructing deployables in the game environment.
* @param obj the deployable
* @param guid the globally unique identifier for the deployable
* @param pos the previous position of the deployable
* @param orient the previous orientation of the deployable
* @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
*/
def DeconstructDeployable(
obj: Deployable,
guid: PlanetSideGUID,
pos: Vector3,
orient: Vector3,
deletionType: Int
): Unit = {
sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
sendResponse(ObjectDeleteMessage(guid, deletionType))
}
} }

View file

@ -2,15 +2,22 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.Tool import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vehicles.MountableWeapons import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg} import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle}
import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons}
import net.psforever.objects.vital.InGameHistory
import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, PlanetSideGUID, Vector3}
// //
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.packet.game.DismountVehicleMsg import net.psforever.packet.game.DismountVehicleMsg
import scala.concurrent.duration._
trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality { trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality {
val ops: SessionMountHandlers val ops: SessionMountHandlers
@ -30,6 +37,219 @@ class SessionMountHandlers(
val avatarActor: typed.ActorRef[AvatarActor.Command], val avatarActor: typed.ActorRef[AvatarActor.Command],
implicit val context: ActorContext implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality { ) extends CommonSessionInterfacingFunctionality {
def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect {
case obj: Mountable =>
obj.Actor ! Mountable.TryMount(player, entry_point)
case _ =>
log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
}
}
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
//TODO optimize this later
//common warning for this section
if (player.GUID == player_guid) {
//normally disembarking from a mount
(sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case out @ Some(obj: Vehicle) =>
continent.GUID(obj.MountedIn) match {
case Some(_: Vehicle) => None //cargo vehicle
case _ => out //arrangement "may" be permissible
}
case out @ Some(_: Mountable) =>
out
case _ =>
dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
None
}) match {
case Some(obj: Mountable) =>
obj.PassengerInSeat(player) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
sessionLogic.zoning.interstellarFerry = None
case None =>
dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
}
case _ =>
dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
}
} else {
//kicking someone else out of a mount; need to own that mount/mountable
val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
player.avatar.vehicle match {
case Some(obj_guid) =>
(
(
sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
) match {
case (vehicle @ Some(obj: Vehicle), tplayer) =>
if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
case (mount @ Some(_: Mountable), tplayer) =>
(mount, tplayer)
case _ =>
(None, None)
}) match {
case (Some(obj: Mountable), Some(tplayer: Player)) =>
obj.PassengerInSeat(tplayer) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
case None =>
dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
}
case (None, _) =>
dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
case (_, None) =>
dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
case _ =>
dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
}
case None =>
dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
}
}
}
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
case Some((mountPoint, _)) =>
cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
case _ =>
log.warn(
s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
)
}
case (None, _) | (Some(_), None) =>
log.warn(
s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
)
case _ => ()
}
}
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt
continent.GUID(cargo_guid) match {
case Some(cargo: Vehicle) =>
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
case _ => ()
}
}
private def dismountWarning(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.warn(note)
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
private def dismountError(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
/**
* Common activities/procedure when a player mounts a valid object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount into which the player is mounting
*/
def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID
val objGuid: PlanetSideGUID = obj.GUID
sessionLogic.actionsToCancel()
avatarActor ! AvatarActor.DeactivateActiveImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
)
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
DismountAction(tplayer, obj, seatNum)
tplayer.WhichSide = OutsideOf
//until vehicles maintain synchronized momentum without a driver
obj match {
case v: Vehicle
if seatNum == 0 =>
/*sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
sessionLogic.vehicles.ServerVehicleOverrideStop(v)
}*/
v.Velocity = Vector3.Zero
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
tplayer.GUID,
v.GUID,
unk1 = 0,
tplayer.Position,
v.Orientation,
v.Velocity,
v.Flying,
unk3 = 0,
unk4 = 0,
wheel_direction = 15,
unk5 = false,
unk6 = v.Cloaked
)
)
case _ => ()
}
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID
tplayer.ContributionFrom(obj)
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
val bailType = if (tplayer.BailProtection) {
BailType.Bailed
} else {
BailType.Normal
}
sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
)
}
/** /**
* From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines.
* @param objWithSeat the object that owns seats (and weaponry) * @param objWithSeat the object that owns seats (and weaponry)

View file

@ -17,6 +17,8 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality { trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality {
val ops: SessionSquadHandlers val ops: SessionSquadHandlers
protected var waypointCooldown: Long = 0L
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit

View file

@ -2,8 +2,12 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.guid.GUIDTask import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.packet.game.FavoritesRequest import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.sourcing.AmenitySource
import net.psforever.objects.vital.TerminalUsedActivity
import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionResultMessage, UnuseItemMessage}
import net.psforever.types.{TransactionType, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
@ -39,6 +43,99 @@ class SessionTerminalHandlers(
private[session] var lastTerminalOrderFulfillment: Boolean = true private[session] var lastTerminalOrderFulfillment: Boolean = true
private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
continent.GUID(terminalGuid) match {
case Some(term: Terminal) if lastTerminalOrderFulfillment =>
val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
lastTerminalOrderFulfillment = false
term.Actor ! Terminal.Request(player, pkt)
case Some(_: Terminal) =>
log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
case Some(obj) =>
log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
case _ =>
log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
}
}
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
val ProximityTerminalUseMessage(_, objectGuid, _) = pkt
continent.GUID(objectGuid) match {
case Some(obj: Terminal with ProximityUnit) =>
performProximityTerminalUse(obj)
case Some(obj) =>
log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
case None =>
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
}
}
def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
val FavoritesRequest(_, loadoutType, action, line, label) = pkt
action match {
case FavoritesAction.Save =>
avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
case FavoritesAction.Delete =>
avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)
case FavoritesAction.Unknown =>
log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action")
}
}
def buyVehicle(
terminalGuid: PlanetSideGUID,
transactionType: TransactionType.Value,
vehicle: Vehicle,
weapons: List[InventoryItem],
trunk: List[InventoryItem]
): Option[Vehicle] = {
continent.map.terminalToSpawnPad
.find { case (termid, _) => termid == terminalGuid.guid }
.map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) }
.collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
vehicle.Faction = player.Faction
vehicle.Position = pad.Position
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
//default loadout, weapons
val vWeapons = vehicle.Weapons
weapons.foreach { entry =>
vWeapons.get(entry.start) match {
case Some(slot) =>
entry.obj.Faction = player.Faction
slot.Equipment = None
slot.Equipment = entry.obj
case None =>
log.warn(
s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
)
}
}
//default loadout, trunk
val vTrunk = vehicle.Trunk
vTrunk.Clear()
trunk.foreach { entry =>
entry.obj.Faction = player.Faction
vTrunk.InsertQuickly(entry.start, entry.obj)
}
TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term))
sendResponse(ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = true))
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
sendResponse(UnuseItemMessage(player.GUID, terminalGuid))
}
player.LogActivity(TerminalUsedActivity(AmenitySource(term), transactionType))
vehicle
}
.orElse {
log.error(
s"${player.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${terminalGuid.guid} to accept it"
)
sendResponse(ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = false))
None
}
}
/** /**
* Construct tasking that adds a completed and registered vehicle into the scene. * Construct tasking that adds a completed and registered vehicle into the scene.
* The major difference between `RegisterVehicle` and `RegisterVehicleFromSpawnPad` is the assumption that this vehicle lacks an internal `Actor`. * The major difference between `RegisterVehicle` and `RegisterVehicleFromSpawnPad` is the assumption that this vehicle lacks an internal `Actor`.
@ -92,7 +189,7 @@ class SessionTerminalHandlers(
* na * na
* @param terminal na * @param terminal na
*/ */
def HandleProximityTerminalUse(terminal: Terminal with ProximityUnit): Unit = { def performProximityTerminalUse(terminal: Terminal with ProximityUnit): Unit = {
val term_guid = terminal.GUID val term_guid = terminal.GUID
val targets = FindProximityUnitTargetsInScope(terminal) val targets = FindProximityUnitTargetsInScope(terminal)
val currentTargets = terminal.Targets val currentTargets = terminal.Targets

View file

@ -39,6 +39,18 @@ class VehicleOperations(
) extends CommonSessionInterfacingFunctionality { ) extends CommonSessionInterfacingFunctionality {
private[session] var serverVehicleControlVelocity: Option[Int] = None private[session] var serverVehicleControlVelocity: Option[Int] = None
/**
* Get the current `Vehicle` object that the player is riding/driving.
* The vehicle must be found solely through use of `player.VehicleSeated`.
* @return the vehicle
*/
def findLocalVehicle: Option[Vehicle] = {
continent.GUID(player.VehicleSeated) match {
case Some(obj: Vehicle) => Some(obj)
case _ => None
}
}
/** /**
* If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
* The priority of object confirmation is `direct` then `occupant.VehicleSeated`. * The priority of object confirmation is `direct` then `occupant.VehicleSeated`.

View file

@ -20,7 +20,9 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.GenericAction.FirstPersonViewWithEffect
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TriggeredSound, WeatherMessage}
import net.psforever.services.chat.DefaultChannel import net.psforever.services.chat.DefaultChannel
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -160,6 +162,30 @@ object ZoningOperations {
events ! LocalServiceMessage(target, soundMessage) events ! LocalServiceMessage(target, soundMessage)
} }
} }
def findBuildingsBySoiOccupancy(zone: Zone, obj: PlanetSideGameObject with BlockMapEntity): List[Building] = {
val positionxy = obj.Position.xy
zone
.blockMap
.sector(obj)
.buildingList
.filter { building =>
val radius = building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius
}
}
def findBuildingsBySoiOccupancy(zone: Zone, position: Vector3): List[Building] = {
val positionxy = position.xy
zone
.blockMap
.sector(positionxy, range=5)
.buildingList
.filter { building =>
val radius = building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius
}
}
} }
object SpawnOperations { object SpawnOperations {
@ -202,6 +228,9 @@ class ZoningOperations(
/** a flag that forces the current zone to reload itself during a zoning operation */ /** a flag that forces the current zone to reload itself during a zoning operation */
private[session] var zoneReload: Boolean = false private[session] var zoneReload: Boolean = false
private[session] val spawn: SpawnOperations = new SpawnOperations() private[session] val spawn: SpawnOperations = new SpawnOperations()
private[session] var maintainInitialGmState: Boolean = false
private[session] var zoneChannel: String = Zone.Nowhere.id
private var loadConfZone: Boolean = false private var loadConfZone: Boolean = false
private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
@ -209,6 +238,7 @@ class ZoningOperations(
private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
private var zoningCounter: Int = 0 private var zoningCounter: Int = 0
private var zoningTimer: Cancellable = Default.Cancellable private var zoningTimer: Cancellable = Default.Cancellable
var displayZoningMessageWhenCancelled: Boolean = true
/* packets */ /* packets */
@ -273,7 +303,7 @@ class ZoningOperations(
continent.VehicleEvents ! Service.Join(continentId) continent.VehicleEvents ! Service.Join(continentId)
continent.VehicleEvents ! Service.Join(factionChannel) continent.VehicleEvents ! Service.Join(factionChannel)
if (sessionLogic.connectionState != 100) configZone(continent) if (sessionLogic.connectionState != 100) configZone(continent)
sendResponse(TimeOfDayMessage(1191182336)) sendResponse(TimeOfDayMessage(1135214592))
//custom //custom
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
@ -618,6 +648,7 @@ class ZoningOperations(
def handleZoneResponse(foundZone: Zone): Unit = { def handleZoneResponse(foundZone: Zone): Unit = {
log.trace(s"ZoneResponse: zone ${foundZone.id} will now load for ${player.Name}") log.trace(s"ZoneResponse: zone ${foundZone.id} will now load for ${player.Name}")
loadConfZone = true loadConfZone = true
maintainInitialGmState = true
val oldZone = session.zone val oldZone = session.zone
session = session.copy(zone = foundZone) session = session.copy(zone = foundZone)
sessionLogic.persist() sessionLogic.persist()
@ -877,12 +908,7 @@ class ZoningOperations(
val location = if (Zones.sanctuaryZoneNumber(player.Faction) == continent.Number) { val location = if (Zones.sanctuaryZoneNumber(player.Faction) == continent.Number) {
Zoning.Time.Sanctuary Zoning.Time.Sanctuary
} else { } else {
val playerPosition = player.Position.xy ZoningOperations.findBuildingsBySoiOccupancy(continent, player.Position) match {
continent.Buildings.values
.filter { building =>
val radius = building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position.xy, playerPosition) < radius * radius
} match {
case Nil => case Nil =>
Zoning.Time.None Zoning.Time.None
case List(building: FactionAffinity) => case List(building: FactionAffinity) =>
@ -916,7 +942,7 @@ class ZoningOperations(
* defaults to `None` * defaults to `None`
*/ */
def CancelZoningProcessWithReason(msg: String, msgType: Option[ChatMessageType] = None): Unit = { def CancelZoningProcessWithReason(msg: String, msgType: Option[ChatMessageType] = None): Unit = {
if (zoningStatus != Zoning.Status.None) { if (displayZoningMessageWhenCancelled && zoningStatus != Zoning.Status.None) {
sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), wideContents=false, "", msg, None)) sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), wideContents=false, "", msg, None))
} }
CancelZoningProcess() CancelZoningProcess()
@ -961,7 +987,7 @@ class ZoningOperations(
*/ */
def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = { def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = {
sendResponse(building.infoUpdateMessage()) sendResponse(building.infoUpdateMessage())
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0))) sendResponse(building.densityLevelUpdateMessage(building))
} }
/** /**
@ -1258,6 +1284,7 @@ class ZoningOperations(
ICS.FindZone(_.id == zoneId, context.self) ICS.FindZone(_.id == zoneId, context.self)
)) ))
} else { } else {
vehicle.Velocity = None
sessionLogic.general.unaccessContainer(vehicle) sessionLogic.general.unaccessContainer(vehicle)
LoadZoneCommonTransferActivity() LoadZoneCommonTransferActivity()
player.VehicleSeated = vehicle.GUID player.VehicleSeated = vehicle.GUID
@ -2155,15 +2182,17 @@ class ZoningOperations(
val map = zone.map val map = zone.map
val mapName = map.name val mapName = map.name
log.info(s"${tplayer.Name} has spawned into $id") log.info(s"${tplayer.Name} has spawned into $id")
sessionLogic.zoning.zoneChannel = Players.ZoneChannelIfSpectating(tplayer, zone.id)
sessionLogic.oldRefsMap.clear() sessionLogic.oldRefsMap.clear()
sessionLogic.persist = UpdatePersistenceAndRefs sessionLogic.persist = UpdatePersistenceAndRefs
tplayer.avatar = avatar tplayer.avatar = avatar
session = session.copy(player = tplayer) session = session.copy(player = tplayer)
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) //val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13"))
sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) sendResponse(LoadMapMessage(mapName, id, 40100, 25, zone.LiveFireAllowed(tplayer.Faction), map.checksum))
if (isAcceptableNextSpawnPoint) { if (isAcceptableNextSpawnPoint) {
//important! the LoadMapMessage must be processed by the client before the avatar is created //important! the LoadMapMessage must be processed by the client before the avatar is created
player.allowInteraction = true
setupAvatarFunc() setupAvatarFunc()
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
sessionLogic.turnCounterFunc = interimUngunnedVehicle match { sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
@ -2201,6 +2230,7 @@ class ZoningOperations(
session = session.copy(player = tplayer) session = session.copy(player = tplayer)
if (isAcceptableNextSpawnPoint) { if (isAcceptableNextSpawnPoint) {
//try this spawn point //try this spawn point
player.allowInteraction = true
setupAvatarFunc() setupAvatarFunc()
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
sessionLogic.turnCounterFunc = interimUngunnedVehicle match { sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
@ -2321,7 +2351,7 @@ class ZoningOperations(
* @param zone na * @param zone na
*/ */
def HandleReleaseAvatar(tplayer: Player, zone: Zone): Unit = { def HandleReleaseAvatar(tplayer: Player, zone: Zone): Unit = {
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Release tplayer.Release
tplayer.VehicleSeated match { tplayer.VehicleSeated match {
case None => case None =>
@ -2466,6 +2496,16 @@ class ZoningOperations(
log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vdef.Name}") log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vdef.Name}")
AvatarCreateInVehicle(player, vehicle, seat) AvatarCreateInVehicle(player, vehicle, seat)
case _ if player.spectator =>
player.VehicleSeated = None
val definition = player.avatar.definition
val guid = player.GUID
sendResponse(OCM.detailed(player))
continent.AvatarEvents ! AvatarServiceMessage(
s"spectator",
AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(player).get, None)
)
case _ => case _ =>
player.VehicleSeated = None player.VehicleSeated = None
val definition = player.avatar.definition val definition = player.avatar.definition
@ -2484,6 +2524,28 @@ class ZoningOperations(
reclaimOurDeployables(continent.DeployableList, player.Name, reassignDeployablesTo(player.GUID)) reclaimOurDeployables(continent.DeployableList, player.Name, reassignDeployablesTo(player.GUID))
) )
) )
//do this to make my deployed telepad appear that way
if (continent.DeployableList.exists(telepad => telepad.Definition == GlobalDefinitions.router_telepad_deployable
&& telepad.OwnerName.contains(player.Name)))
{
continent.Vehicles.filter(router => router.Definition == GlobalDefinitions.router && router.Faction == player.Faction)
.foreach { obj =>
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
}
}
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215),
StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215),
StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215),
StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215),
StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215),
StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215),
StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215),
StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215),
StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215),
StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215))))
//begin looking for conditions to set the avatar //begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
} }
@ -2593,8 +2655,20 @@ class ZoningOperations(
sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data))
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
} }
avatarActor ! AvatarActor.RefreshPurchaseTimes()
setupAvatarFunc = AvatarCreate setupAvatarFunc = AvatarCreate
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215),
StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215),
StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215),
StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215),
StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215),
StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215),
StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215),
StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215),
StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215),
StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215))))
//begin looking for conditions to set the avatar //begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
} }
@ -2614,8 +2688,6 @@ class ZoningOperations(
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife)) zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer) val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj) DefinitionUtil.applyDefaultLoadout(obj)
obj.death_by = tplayer.death_by
obj.silenced = tplayer.silenced
obj obj
} }
@ -2894,7 +2966,6 @@ class ZoningOperations(
) )
) )
nextSpawnPoint = physSpawnPoint nextSpawnPoint = physSpawnPoint
prevSpawnPoint = physSpawnPoint
shiftPosition = Some(pos) shiftPosition = Some(pos)
shiftOrientation = Some(ori) shiftOrientation = Some(ori)
val toZoneNumber = if (continent.id.equals(zoneId)) { val toZoneNumber = if (continent.id.equals(zoneId)) {
@ -2931,10 +3002,10 @@ class ZoningOperations(
case _ if player.HasGUID => // player is deconstructing self or instant action case _ if player.HasGUID => // player is deconstructing self or instant action
val player_guid = player.GUID val player_guid = player.GUID
sendResponse(ObjectDeleteMessage(player_guid, 4)) sendResponse(ObjectDeleteMessage(player_guid, unk1=1))
continent.AvatarEvents ! AvatarServiceMessage( continent.AvatarEvents ! AvatarServiceMessage(
continent.id, continent.id,
AvatarAction.ObjectDelete(player_guid, player_guid, 4) AvatarAction.ObjectDelete(player_guid, player_guid, unk=1)
) )
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId) LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
@ -2950,7 +3021,7 @@ class ZoningOperations(
/** /**
* na * na
* @param target player being spawned * @param target player being spawned
* @param position where player is being placed in the game wqrld * @param position where player is being placed in the game world
* @param orientation in what direction the player is facing in the game world * @param orientation in what direction the player is facing in the game world
* @param onThisSide description of the containing environment * @param onThisSide description of the containing environment
* @param goingToZone common designation for the zone * @param goingToZone common designation for the zone
@ -3040,10 +3111,11 @@ class ZoningOperations(
sessionLogic.keepAliveFunc = sessionLogic.vehicles.GetMountableAndSeat(None, player, continent) match { sessionLogic.keepAliveFunc = sessionLogic.vehicles.GetMountableAndSeat(None, player, continent) match {
case (Some(v: Vehicle), Some(seatNumber)) case (Some(v: Vehicle), Some(seatNumber))
if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty => if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty =>
sessionLogic.keepAlivePersistence sessionLogic.keepAlivePersistenceFunc
case _ => case _ =>
NormalKeepAlive NormalKeepAlive
} }
prevSpawnPoint = nextSpawnPoint
nextSpawnPoint = None nextSpawnPoint = None
} }
//if not the condition above, player has started playing normally //if not the condition above, player has started playing normally
@ -3118,7 +3190,11 @@ class ZoningOperations(
statisticsPacketFunc() statisticsPacketFunc()
if (tplayer.ExoSuit == ExoSuitType.MAX) { if (tplayer.ExoSuit == ExoSuitType.MAX) {
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor))
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, tplayer.Armor))
} }
// for issue #1269
continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor)))
// AvatarAwardMessage // AvatarAwardMessage
//populateAvatarAwardRibbonsFunc(1, 20L) //populateAvatarAwardRibbonsFunc(1, 20L)
@ -3242,10 +3318,10 @@ class ZoningOperations(
upstreamMessageCount = 0 upstreamMessageCount = 0
if (tplayer.spectator) { if (tplayer.spectator) {
if (!setAvatar) { if (!setAvatar) {
context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status context.self ! SessionActor.SetMode(sessionLogic.chat.CurrentSpectatorMode) //should reload spectator status
} }
} else if ( } else if (
!account.gm && /* gm's are excluded */ !avatar.permissions.canGM && /* gm's are excluded */
Config.app.game.promotion.active && /* play versus progress system must be active */ Config.app.game.promotion.active && /* play versus progress system must be active */
BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */ BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */
tavatar.scorecard.Lives.isEmpty && /* first life after login */ tavatar.scorecard.Lives.isEmpty && /* first life after login */
@ -3255,6 +3331,7 @@ class ZoningOperations(
enqueueNewActivity(ActivityQueuedTask(ZoningOperations.reportProgressionSystem, 2)) enqueueNewActivity(ActivityQueuedTask(ZoningOperations.reportProgressionSystem, 2))
} }
} }
avatarActor ! AvatarActor.RefreshPurchaseTimes()
} }
/** /**
@ -3492,7 +3569,7 @@ class ZoningOperations(
//sit down //sit down
sendResponse(ObjectAttachMessage(vguid, pguid, seat)) sendResponse(ObjectAttachMessage(vguid, pguid, seat))
sessionLogic.general.accessContainer(vehicle) sessionLogic.general.accessContainer(vehicle)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
case _ => () case _ => ()
//we can't find a vehicle? and we're still here? that's bad //we can't find a vehicle? and we're still here? that's bad
player.VehicleSeated = None player.VehicleSeated = None
@ -3728,6 +3805,14 @@ class ZoningOperations(
player.death_by = 1 player.death_by = 1
} }
GoToDeploymentMap() GoToDeploymentMap()
val pZone = player.Zone
sendResponse(GenericActionMessage(FirstPersonViewWithEffect))
pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs))
}
pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction =>
pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs))
}
} }
def stopDeconstructing(): Unit = { def stopDeconstructing(): Unit = {

View file

@ -74,6 +74,8 @@ object BuildingActor {
final case class PowerOff() extends Command final case class PowerOff() extends Command
final case class DensityLevelUpdate(building: Building) extends Command
/** /**
* Set a facility affiliated to one faction to be affiliated to a different faction. * Set a facility affiliated to one faction to be affiliated to a different faction.
* @param details building and event system references * @param details building and event system references
@ -226,6 +228,7 @@ class BuildingActor(
case MapUpdate() => case MapUpdate() =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building)))
Behaviors.same Behaviors.same
case AmenityStateChange(amenity, data) => case AmenityStateChange(amenity, data) =>
@ -245,6 +248,10 @@ class BuildingActor(
case Ntu(msg) => case Ntu(msg) =>
logic.ntu(details, msg) logic.ntu(details, msg)
case DensityLevelUpdate(building) =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building)))
Behaviors.same
} }
} }
} }

View file

@ -182,7 +182,8 @@ class ZoneActor(
case ZoneMapUpdate() => case ZoneMapUpdate() =>
zone.Buildings zone.Buildings
.filter(_._2.BuildingType == StructureType.Facility) .filter(building =>
building._2.BuildingType == StructureType.Facility || building._2.BuildingType == StructureType.Tower)
.values .values
.foreach(_.Actor ! BuildingActor.MapUpdate()) .foreach(_.Actor ! BuildingActor.MapUpdate())
Behaviors.same Behaviors.same

View file

@ -1067,6 +1067,13 @@ object WorldSession {
} }
} }
/**
* Perform a task and, upon completion, dispatch a message.
* @param task task to be completed first
* @param sendTo where to send the message
* @param pass message
* @return a `TaskBundle` object
*/
def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any): TaskBundle = { def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any): TaskBundle = {
TaskBundle( TaskBundle(
new StraightforwardTask() { new StraightforwardTask() {
@ -1085,6 +1092,14 @@ object WorldSession {
) )
} }
/**
* Perform a task and, upon completion, dispatch a message whose origin is specific and different from normal.
* @param task task to be completed first
* @param sendTo where to send the message
* @param pass message
* @param replyTo whom to attribute the message being passed (e.g., `tell`)
* @return a `TaskBundle` object
*/
def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any, replyTo: ActorRef): TaskBundle = { def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any, replyTo: ActorRef): TaskBundle = {
TaskBundle( TaskBundle(
new StraightforwardTask() { new StraightforwardTask() {

View file

@ -0,0 +1,61 @@
// Copyright (c) 2025 PSForever
package net.psforever.objects
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution, DamageType}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition}
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
final case class OrbitalStrike(player: PlayerSource)
extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Hit
def same(test: DamageReason): Boolean = {
test.source eq source
}
def source: DamageProperties = OrbitalStrike.source
def damageModel: DamageAndResistance = OrbitalStrike.drm
override def adversary : Option[SourceEntry] = Some(player)
}
object OrbitalStrike {
final val cr4_os = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 10000
DamageAtEdge = 0.1f
DamageRadius = 10f
}
final val cr5_os = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 10000
DamageAtEdge = 0.1f
DamageRadius = 20f
}
private val source = new DamageProperties {
Damage0 = 10000
Damage1 = 10000
Damage2 = 10000
Damage3 = 10000
Damage4 = 10000
DamageToHealthOnly = true
DamageToVehicleOnly = true
DamageToBattleframeOnly = true
}
private val drm = new DamageResistanceModel {
DamageUsing = DamageCalculations.AgainstExoSuit
ResistUsing = NoResistanceSelection
Model = SimpleResolutions.calculate
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.avatar.interaction.{WithEntrance, WithGantry, WithLava, WithWater} import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
@ -47,7 +47,7 @@ class Player(var avatar: Avatar)
new WithGantry(avatar.name), new WithGantry(avatar.name),
new WithMovementTrigger() new WithMovementTrigger()
))) )))
interaction(new InteractWithMines(range = 10)) interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
@ -81,6 +81,7 @@ class Player(var avatar: Avatar)
Continent = "home2" //the zone id Continent = "home2" //the zone id
var spectator: Boolean = false var spectator: Boolean = false
var bops: Boolean = false
var silenced: Boolean = false var silenced: Boolean = false
var death_by: Int = 0 var death_by: Int = 0
var lastShotSeq_time: Int = -1 var lastShotSeq_time: Int = -1
@ -634,13 +635,16 @@ object Player {
player.Inventory.Resize(eSuit.InventoryScale.Width, eSuit.InventoryScale.Height) player.Inventory.Resize(eSuit.InventoryScale.Width, eSuit.InventoryScale.Height)
player.Inventory.Offset = eSuit.InventoryOffset player.Inventory.Offset = eSuit.InventoryOffset
//holsters //holsters
(0 until 5).foreach(index => { player.Slot(index).Size = eSuit.Holster(index) }) (0 until 5).foreach { index => player.Slot(index).Size = eSuit.Holster(index) }
} }
def Respawn(player: Player): Player = { def Respawn(player: Player): Player = {
if (player.Release) { if (player.Release) {
val obj = new Player(player.avatar) val obj = new Player(player.avatar)
obj.Continent = player.Continent obj.Continent = player.Continent
obj.death_by = player.death_by
obj.silenced = player.silenced
obj.allowInteraction = player.allowInteraction
obj.avatar.scorecard.respawn() obj.avatar.scorecard.respawn()
obj obj
} else { } else {

View file

@ -18,7 +18,7 @@ import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{InGameActivity, InGameHistory, RevivingActivity} import net.psforever.objects.vital.{InGameActivity, InGameHistory, RevivingActivity}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3} import net.psforever.types.{ChatMessageType, ExoSuitType, PlanetSideGUID, Vector3}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -80,6 +80,8 @@ object Players {
) )
) )
target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID)) target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID))
val reviveMessage = s"@YouHaveBeenMessage^revived~^$medicName~"
PlayerControl.sendResponse(target.Zone, name, ChatMsg(ChatMessageType.UNK_227, reviveMessage))
} }
/** /**
@ -327,7 +329,7 @@ object Players {
*/ */
def successfulBuildActivity(zone: Zone, channel: String, obj: Deployable): Unit = { def successfulBuildActivity(zone: Zone, channel: String, obj: Deployable): Unit = {
//sent to avatar event bus to preempt additional tool management //sent to avatar event bus to preempt additional tool management
buildCooldownReset(zone, channel, obj) buildCooldownReset(zone, channel, obj.GUID)
//sent to local event bus to cooperate with deployable management //sent to local event bus to cooperate with deployable management
zone.LocalEvents ! LocalServiceMessage( zone.LocalEvents ! LocalServiceMessage(
channel, channel,
@ -339,13 +341,13 @@ object Players {
* Common actions related to constructing a new `Deployable` object in the game environment. * Common actions related to constructing a new `Deployable` object in the game environment.
* @param zone in which zone these messages apply * @param zone in which zone these messages apply
* @param channel to whom to send the messages * @param channel to whom to send the messages
* @param obj the `Deployable` object * @param guid `Deployable` object
*/ */
def buildCooldownReset(zone: Zone, channel: String, obj: Deployable): Unit = { def buildCooldownReset(zone: Zone, channel: String, guid: PlanetSideGUID): Unit = {
//sent to avatar event bus to preempt additional tool management //sent to avatar event bus to preempt additional tool management
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
channel, channel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(obj.GUID, 21)) AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(guid, 21))
) )
} }
@ -488,4 +490,35 @@ object Players {
} }
player.HistoryAndContributions() player.HistoryAndContributions()
} }
/**
* Select the player's zone channel.
* If the player is spectating, then that is their channel instead.
* The resulting channel name should never be used for subscribing - only for publishing.
* @param player player in a zone
* @return channel name
*/
def ZoneChannelIfSpectating(player: Player): String = {
if (player.spectator) {
"spectator"
} else {
player.Zone.id
}
}
/**
* Select the player's zone channel.
* If the player is spectating, then that is their channel instead.
* The resulting channel name should never be used for subscribing - only for publishing.
* @param player player in a zone
* @param zoneid custom zone name to be used as the channel name
* @return channel name
*/
def ZoneChannelIfSpectating(player: Player, zoneid: String): String = {
if (player.spectator) {
"spectator"
} else {
zoneid
}
}
} }

View file

@ -140,8 +140,6 @@ object SpawnPoint {
def CavernGate(innerRadius: Float)(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = { def CavernGate(innerRadius: Float)(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = {
val (a, b) = metaGate(obj, target, innerRadius) val (a, b) = metaGate(obj, target, innerRadius)
target match { target match {
case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) =>
(a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b)
case m: MountableEntity => case m: MountableEntity =>
m.BailProtection = true m.BailProtection = true
(a + Vector3.z(obj.Definition.UseRadius * 0.5f), b) (a + Vector3.z(obj.Definition.UseRadius * 0.5f), b)
@ -154,7 +152,7 @@ object SpawnPoint {
val (a, b) = metaGate(obj, target, innerRadius) val (a, b) = metaGate(obj, target, innerRadius)
target match { target match {
case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) => case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) =>
(a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b) (a, b)
case _ => case _ =>
(a + Vector3.z(flightlessZOffset), b) (a + Vector3.z(flightlessZOffset), b)
} }

View file

@ -65,6 +65,108 @@ object SpecialEmp {
innateDamage = emp innateDamage = emp
} }
final val cr3_emp = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 10f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
Modifiers = MaxDistanceCutoff
}
final val cr4_emp = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 15f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
Modifiers = MaxDistanceCutoff
}
final val cr5_emp = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 20f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
Modifiers = MaxDistanceCutoff
}
/** /**
* Trigger an electromagnetic pulse. * Trigger an electromagnetic pulse.
*/ */

View file

@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness} import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness}
import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.interaction.{WithLava, WithWater} import net.psforever.objects.vehicles.interaction.{TriggerOnVehicleRule, WithLava, WithWater}
import net.psforever.objects.vital.resistance.StandardResistanceProfile import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -101,7 +101,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
new WithDeath(), new WithDeath(),
new WithMovementTrigger() new WithMovementTrigger()
))) )))
interaction(new InteractWithMines(range = 20)) interaction(new InteractWithMines(range = 20, TriggerOnVehicleRule))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))

View file

@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.transfer.TransferContainer
import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{ChatMsg, HackMessage, HackState, HackState1, HackState7, TriggeredSound} import net.psforever.packet.game.{ChatMsg, FrameVehicleStateMessage, GenericObjectActionEnum, GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound, VehicleStateMessage}
import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -42,11 +42,13 @@ object Vehicles {
case Some(tplayer) => case Some(tplayer) =>
tplayer.avatar.vehicle = Some(vehicle.GUID) tplayer.avatar.vehicle = Some(vehicle.GUID)
vehicle.AssignOwnership(playerOpt) vehicle.AssignOwnership(playerOpt)
val locked = VehicleLockState.Locked.id
Array(0, 3).foreach(group => vehicle.PermissionGroup(group, locked))
Vehicles.ReloadAccessPermissions(vehicle, tplayer.Faction.toString)
vehicle.Zone.VehicleEvents ! VehicleServiceMessage( vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
vehicle.Zone.id, vehicle.Zone.id,
VehicleAction.Ownership(tplayer.GUID, vehicle.GUID) VehicleAction.Ownership(tplayer.GUID, vehicle.GUID)
) )
Vehicles.ReloadAccessPermissions(vehicle, tplayer.Faction.toString)
Some(vehicle) Some(vehicle)
case None => case None =>
None None
@ -272,6 +274,20 @@ object Vehicles {
VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, tGuid) VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, tGuid)
) )
} }
// In case BFR is occupied and may or may not be crouched
if (GlobalDefinitions.isBattleFrameVehicle(target.Definition) && target.Seat(0).isDefined) {
zone.LocalEvents ! LocalServiceMessage(
zoneid,
LocalAction.SendGenericObjectActionMessage(PlanetSideGUID(-1), target.GUID, GenericObjectActionEnum.BFRShieldsDown))
zone.LocalEvents ! LocalServiceMessage(
zoneid,
LocalAction.SendResponse(
FrameVehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), unk2=false, 0, 0, is_crouched=true, is_airborne=false, ascending_flight=false, 10, 0, 0)))
zone.LocalEvents ! LocalServiceMessage(
zoneid,
LocalAction.SendResponse(
VehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), None, 0, 0, 15, is_decelerating=false, is_cloaked=false)))
}
}) })
// If the vehicle can fly and is flying: deconstruct it; and well played to whomever managed to hack a plane in mid air // If the vehicle can fly and is flying: deconstruct it; and well played to whomever managed to hack a plane in mid air
if (target.Definition.CanFly && target.isFlying) { if (target.Definition.CanFly && target.isFlying) {

View file

@ -68,6 +68,13 @@ object Avatar {
GlobalDefinitions.trhev_pounder -> 5.minutes GlobalDefinitions.trhev_pounder -> 5.minutes
) )
val cudCooldowns: Map[String, FiniteDuration] = Map(
"orbital_strike" -> 3.hours,
"emp_blast" -> 20.minutes,
"reveal_friendlies" -> 20.minutes,
"reveal_enemies" -> 20.minutes
)
val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map( val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map(
GlobalDefinitions.medkit -> 5.seconds, GlobalDefinitions.medkit -> 5.seconds,
GlobalDefinitions.super_armorkit -> 20.minutes, GlobalDefinitions.super_armorkit -> 20.minutes,

View file

@ -3,9 +3,9 @@ package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props, typed} import akka.actor.{Actor, ActorRef, Props, typed}
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.avatar.PlayerControl.sendResponse
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.DeployAnimation import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.definition.converter.OCM import net.psforever.objects.definition.converter.OCM
@ -18,7 +18,6 @@ import net.psforever.objects.serverobject.containable.{Containable, ContainableB
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.vital._ import net.psforever.objects.vital._
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
@ -35,8 +34,9 @@ import net.psforever.objects.serverobject.repair.Repairable
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource} import net.psforever.objects.sourcing.{AmenitySource, PlayerSource}
import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.collision.CollisionReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import org.joda.time.{LocalDateTime, Seconds}
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -149,6 +149,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
newHealth - originalHealth newHealth - originalHealth
) )
) )
val amount = newHealth - originalHealth
val healMessageSelf = s"@SelfHitHealedMessage^healed~^$amount~^health~"
val healMessageOther = s"@WereHitByHealedMessage^healed~^$amount~^health~^$uname~"
if (player == user) {
sendResponse(user.Zone, user.Name, ChatMsg(ChatMessageType.UNK_227, healMessageSelf))
}
else {
sendResponse(player.Zone, player.Name, ChatMsg(ChatMessageType.UNK_227, healMessageOther))
}
} }
if (player != user) { if (player != user) {
//"Someone is trying to heal you" //"Someone is trying to heal you"
@ -212,6 +221,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
newArmor - originalArmor newArmor - originalArmor
) )
) )
val amount = newArmor - originalArmor
val repairMessageSelf = s"@SelfHitHealedMessage^repaired~^$amount~^armor~"
val repairMessageOther = s"@WereHitByHealedMessage^repaired~^$amount~^armor~^$uname~"
if (player == user) {
sendResponse(user.Zone, user.Name, ChatMsg(ChatMessageType.UNK_227, repairMessageSelf))
}
else {
sendResponse(player.Zone, player.Name, ChatMsg(ChatMessageType.UNK_227, repairMessageOther))
}
} }
if (player != user) { if (player != user) {
if (player.isAlive) { if (player.isAlive) {
@ -320,7 +338,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
} else if ((!resistance && before != slot && (player.DrawnSlot = slot) != before) && ItemSwapSlot != before) { } else if ((!resistance && before != slot && (player.DrawnSlot = slot) != before) && ItemSwapSlot != before) {
val mySlot = if (updateMyHolsterArm) slot else -1 //use as a short-circuit val mySlot = if (updateMyHolsterArm) slot else -1 //use as a short-circuit
events ! AvatarServiceMessage( events ! AvatarServiceMessage(
player.Continent, Players.ZoneChannelIfSpectating(player),
AvatarAction.ObjectHeld(player.GUID, mySlot, player.LastDrawnSlot) AvatarAction.ObjectHeld(player.GUID, mySlot, player.LastDrawnSlot)
) )
val isHolsters = player.VisibleSlots.contains(slot) val isHolsters = player.VisibleSlots.contains(slot)
@ -332,16 +350,21 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) {
//rek beam/icon colour must match the player's correct hack level //rek beam/icon colour must match the player's correct hack level
events ! AvatarServiceMessage( events ! AvatarServiceMessage(
player.Continent, Players.ZoneChannelIfSpectating(player),
AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel()) AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel())
) )
} }
case None => ; case None => ()
} }
} else { } else {
equipment match { equipment match {
case Some(holsteredEquipment) => case Some(holsteredEquipment) =>
log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down")
//make sure the player didn't just initialte an orbital strike. If not (the if below is true), make sure waypoint is removed
if (holsteredEquipment.Definition == GlobalDefinitions.command_detonater && player.avatar.cr.value > 3 &&
!player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 12)) {
player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Faction}", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None)))
}
case None => case None =>
log.info(s"${player.Name} lowers ${player.Sex.possessive} hand") log.info(s"${player.Name} lowers ${player.Sex.possessive} hand")
} }
@ -385,15 +408,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
} }
if (Players.CertificationToUseExoSuit(player, exosuit, subtype)) { if (Players.CertificationToUseExoSuit(player, exosuit, subtype)) {
if (exosuit == ExoSuitType.MAX) { if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) val cooldown = player.avatar.purchaseCooldown(GlobalDefinitions.MAXArms(subtype, player.Faction))
val cooldown = player.avatar.purchaseCooldown(weapon)
if (originalSubtype == subtype) { if (originalSubtype == subtype) {
(exosuit, subtype) //same MAX subtype is free (exosuit, subtype) //same MAX subtype
} else if (cooldown.nonEmpty) { } else if (cooldown.nonEmpty) {
fallbackSuit //different MAX subtype can not have cooldown fallbackSuit //different MAX subtype
} else { } else {
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) (exosuit, subtype) //switching
(exosuit, subtype) //switching for first time causes cooldown
} }
} else { } else {
(exosuit, subtype) (exosuit, subtype)
@ -475,11 +496,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case InventoryItem(citem: ConstructionItem, _) => case InventoryItem(citem: ConstructionItem, _) =>
Deployables.initializeConstructionItem(player.avatar.certifications, citem) Deployables.initializeConstructionItem(player.avatar.certifications, citem)
} }
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants
val zone = player.Zone val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, Players.ZoneChannelIfSpectating(player),
AvatarAction.ChangeLoadout( AvatarAction.ChangeLoadout(
player.GUID, player.GUID,
toArmor, toArmor,
@ -556,7 +575,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//don't know where boomer trigger "should" go //don't know where boomer trigger "should" go
TaskWorkflow.execute(PutNewEquipmentInInventoryOrDrop(player)(trigger)) TaskWorkflow.execute(PutNewEquipmentInInventoryOrDrop(player)(trigger))
} }
Players.buildCooldownReset(zone, player.Name, obj) Players.buildCooldownReset(zone, player.Name, obj.GUID)
case _ => () case _ => ()
} }
deployablePair = None deployablePair = None
@ -573,7 +592,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Actor ! Player.LoseDeployable(obj) player.Actor ! Player.LoseDeployable(obj)
TelepadControl.TelepadError(zone, player.Name, msg = "@Telepad_NoDeploy_RouterLost") TelepadControl.TelepadError(zone, player.Name, msg = "@Telepad_NoDeploy_RouterLost")
} }
Players.buildCooldownReset(zone, player.Name, obj) Players.buildCooldownReset(zone, player.Name, obj.GUID)
case _ => () case _ => ()
} }
deployablePair = None deployablePair = None
@ -581,7 +600,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case Zone.Deployable.IsBuilt(obj) => case Zone.Deployable.IsBuilt(obj) =>
deployablePair match { deployablePair match {
case Some((deployable, tool)) if deployable eq obj => case Some((deployable, tool)) if deployable eq obj =>
Players.buildCooldownReset(player.Zone, player.Name, obj) Players.buildCooldownReset(player.Zone, player.Name, obj.GUID)
player.Find(tool) match { player.Find(tool) match {
case Some(index) => case Some(index) =>
Players.commonDestroyConstructionItem(player, tool, index) Players.commonDestroyConstructionItem(player, tool, index)
@ -613,10 +632,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) player.avatar.purchaseCooldown(weapon)
.collect(_ => false) .collect(_ => false)
.getOrElse { .getOrElse(true)
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else { } else {
true true
}) })
@ -671,10 +687,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//insert //insert
afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj) afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj)
afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj)) afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj))
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants
player.Zone.AvatarEvents ! AvatarServiceMessage( player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id, Players.ZoneChannelIfSpectating(player),
AvatarAction.ChangeExosuit( AvatarAction.ChangeExosuit(
player.GUID, player.GUID,
toArmor, toArmor,
@ -744,7 +758,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
else { else {
log.warn(s"cannot build a ${obj.Definition.Name}") log.warn(s"cannot build a ${obj.Definition.Name}")
DropEquipmentFromInventory(player)(tool, Some(obj.Position)) DropEquipmentFromInventory(player)(tool, Some(obj.Position))
Players.buildCooldownReset(zone, player.Name, obj) Players.buildCooldownReset(zone, player.Name, obj.GUID)
obj.Position = Vector3.Zero obj.Position = Vector3.Zero
obj.AssignOwnership(None) obj.AssignOwnership(None)
zone.Deployables ! Zone.Deployable.Dismiss(obj) zone.Deployables ! Zone.Deployable.Dismiss(obj)
@ -755,7 +769,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
obj.AssignOwnership(None) obj.AssignOwnership(None)
val zone = player.Zone val zone = player.Zone
zone.Deployables ! Zone.Deployable.Dismiss(obj) zone.Deployables ! Zone.Deployable.Dismiss(obj)
Players.buildCooldownReset(zone, player.Name, obj) Players.buildCooldownReset(zone, player.Name, obj.GUID)
} }
} }
@ -954,7 +968,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
def DestructionAwareness(target: Player, cause: DamageResult): Unit = { def DestructionAwareness(target: Player, cause: DamageResult): Unit = {
val player_guid = target.GUID val player_guid = target.GUID
val pos = target.Position val pos = target.Position
val respawnTimer = 300000 //milliseconds
val zone = target.Zone val zone = target.Zone
val events = zone.AvatarEvents val events = zone.AvatarEvents
val nameChannel = target.Name val nameChannel = target.Name
@ -980,36 +993,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
damageLog.info(s"${player.Name} killed ${player.Sex.pronounObject}self") damageLog.info(s"${player.Name} killed ${player.Sex.pronounObject}self")
} }
// This would normally happen async as part of AvatarAction.Killed, but if it doesn't happen before deleting calling AvatarAction.ObjectDelete on the player the LLU will end up invisible to others if carried events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, cause, target.VehicleSeated)) //align client interface fields with state
// Therefore, queue it up to happen first.
events ! AvatarServiceMessage(nameChannel, AvatarAction.DropSpecialItem())
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.Killed(player_guid, target.VehicleSeated)
) //align client interface fields with state
zone.GUID(target.VehicleSeated) match {
case Some(obj: Mountable) =>
//boot cadaver from mount internally (vehicle perspective)
obj.PassengerInSeat(target) match {
case Some(index) =>
obj.Seats(index).unmount(target)
case _ => ;
}
//boot cadaver from mount on client
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero)
)
)
//make player invisible on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1))
//only the dead player should "see" their own body, so that the death camera has something to focus on
events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid))
case _ => ;
}
events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health
if (target.Capacitor > 0) { if (target.Capacitor > 0) {
target.Capacitor = 0 target.Capacitor = 0
@ -1023,35 +1007,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos) DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos)
) //how many players get this message? ) //how many players get this message?
) )
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, unk5=true)
)
)
//TODO other methods of death?
val pentry = PlayerSource(target)
cause
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
.orElse {
target.LastDamage.collect {
case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis =>
attack
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
}.flatten
} match {
case Some(adversarial) =>
events ! AvatarServiceMessage(
zoneChannel,
AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement)
)
case _ =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
zone.actor ! ZoneActor.RewardThisDeath(player)
} }
def suicide() : Unit = { def suicide() : Unit = {

View file

@ -0,0 +1,14 @@
// Copyright (c) 2025 PSForever
package net.psforever.objects.avatar.interaction
import net.psforever.objects.ce.TriggerTest
import net.psforever.objects.ExplosiveDeployable
import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.zones.Zone
case object TriggerOnPlayerRule
extends TriggerTest {
def test(g: VolumetricGeometry, obj: ExplosiveDeployable, radius: Float): Boolean = {
Zone.distanceCheck(g, obj, radius)
}
}

View file

@ -81,24 +81,7 @@ final case class Projectile(
* @return a new `Projectile` entity * @return a new `Projectile` entity
*/ */
def quality(value: ProjectileQuality): Projectile = { def quality(value: ProjectileQuality): Projectile = {
val projectile = new Projectile( Projectile.copy(original=this, copy(quality = value))
profile,
tool_def,
fire_mode,
mounted_in,
owner,
attribute_to,
shot_origin,
shot_angle,
shot_velocity,
value,
id,
fire_time
)
if(isMiss) projectile.Miss()
else if(isResolved) projectile.Resolve()
projectile.WhichSide = this.WhichSide
projectile
} }
/** /**
@ -186,4 +169,15 @@ object Projectile {
): Projectile = { ): Projectile = {
Projectile(profile, tool_def, fire_mode, None, owner, attribute_to, shot_origin, shot_angle, None) Projectile(profile, tool_def, fire_mode, None, owner, attribute_to, shot_origin, shot_angle, None)
} }
def copy(original: Projectile, dirtyCopy: Projectile): Projectile = {
val properCopy = dirtyCopy.copy(fire_time = original.fire_time, id = original.id)
properCopy.GUID = original.GUID
properCopy.WhichSide = original.WhichSide
if (original.isMiss)
properCopy.Miss()
else if (original.isResolved)
properCopy.Resolve()
properCopy
}
} }

View file

@ -68,7 +68,12 @@ trait DeployableBehavior {
if DeployableObject.OwnerGuid.nonEmpty => if DeployableObject.OwnerGuid.nonEmpty =>
val obj = DeployableObject val obj = DeployableObject
if (constructed.contains(true)) { if (constructed.contains(true)) {
if (obj.Definition.DeployCategory == DeployableCategory.Boomers) {
loseOwnership(obj, PlanetSideEmpire.NEUTRAL) loseOwnership(obj, PlanetSideEmpire.NEUTRAL)
}
else {
loseOwnership(obj, obj.Faction)
}
} else { } else {
obj.OwnerGuid = None obj.OwnerGuid = None
} }
@ -290,6 +295,11 @@ object DeployableBehavior {
originalFaction.toString, originalFaction.toString,
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info) LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
) )
//remove deployable from original owner's toolbox and UI counter
zone.AllPlayers.filter(p => obj.OriginalOwnerName.contains(p.Name))
.foreach { originalOwner =>
originalOwner.avatar.deployables.Remove(obj)
originalOwner.Zone.LocalEvents ! LocalServiceMessage(originalOwner.Name, LocalAction.DeployableUIFor(obj.Definition.Item))
} }
//display to the given faction //display to the given faction
localEvents ! LocalServiceMessage( localEvents ! LocalServiceMessage(
@ -298,3 +308,4 @@ object DeployableBehavior {
) )
} }
} }
}

View file

@ -1,8 +1,9 @@
// Copyright (c) 2021 PSForever // Copyright (c) 2021 PSForever
package net.psforever.objects.ce package net.psforever.objects.ce
import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.zones.blockmap.SectorPopulation import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType}
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable} import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable}
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
@ -13,7 +14,7 @@ case object MineInteraction extends ZoneInteractionType
* "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion * "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion
* and maybe death. * and maybe death.
*/ */
class InteractWithMines(val range: Float) class InteractWithMines(val range: Float, rule: TriggerTest)
extends ZoneInteraction { extends ZoneInteraction {
/** /**
* mines that, though detected, are skipped from being alerted; * mines that, though detected, are skipped from being alerted;
@ -23,7 +24,7 @@ class InteractWithMines(val range: Float)
*/ */
private var skipTargets: List[PlanetSideGUID] = List() private var skipTargets: List[PlanetSideGUID] = List()
def Type = MineInteraction def Type: MineInteraction.type = MineInteraction
/** /**
* Trample upon active mines in our current detection sector and alert those mines. * Trample upon active mines in our current detection sector and alert those mines.
@ -32,12 +33,12 @@ class InteractWithMines(val range: Float)
*/ */
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val faction = target.Faction val faction = target.Faction
lazy val targetGeometry = target.Definition.Geometry(target)
val targets = sector val targets = sector
.deployableList .deployableList
.filter { .filter {
case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here case _: BoomerDeployable => false //boomers are a specific type of ExplosiveDeployable that do not count here
case ex: ExplosiveDeployable => ex.Faction != faction && case ex: ExplosiveDeployable => ex.Faction != faction && rule.test(targetGeometry, ex, ex.Definition.triggerRadius)
Zone.distanceCheck(target, ex, ex.Definition.triggerRadius)
case _ => false case _ => false
} }
val notSkipped = targets.filterNot { t => skipTargets.contains(t.GUID) } val notSkipped = targets.filterNot { t => skipTargets.contains(t.GUID) }
@ -56,3 +57,19 @@ class InteractWithMines(val range: Float)
skipTargets = List() skipTargets = List()
} }
} }
/**
* The testing rule used to determine if a target is within range
* to agitate the game world deployable extra-territorial munitions.
*/
trait TriggerTest {
/**
* Perform the test
* @param g the geometric representation of a game entity
* @param obj a game entity
* @param distance the maximum distance permissible between game entities
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def test(g: VolumetricGeometry, obj: ExplosiveDeployable, distance: Float): Boolean
}

View file

@ -4,6 +4,7 @@ package net.psforever.objects.definition
abstract class BasicDefinition { abstract class BasicDefinition {
private var name: String = "definition" private var name: String = "definition"
private var descriptor: Option[String] = None private var descriptor: Option[String] = None
private var mapRevealId: Int = 0
def Name: String = name def Name: String = name
@ -20,4 +21,11 @@ abstract class BasicDefinition {
descriptor = description descriptor = description
Descriptor Descriptor
} }
def MapRevealId: Int = mapRevealId //for vehicle IDs used by reveal friendlies and enemies with a CUD
def MapRevealId_=(mapRevealId: Int): Int = {
this.mapRevealId = mapRevealId
MapRevealId
}
} }

View file

@ -65,11 +65,17 @@ object AvatarConverter {
*/ */
def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = { def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = {
val alt_model_flag: Boolean = obj.isBackpack val alt_model_flag: Boolean = obj.isBackpack
val avatar = obj.avatar
val tempAvatarInfo = if (obj.spectator) {
avatar.basic.copy(name = s"<spectator:${avatar.basic.name}>")
} else {
avatar.basic
}
val aa: Int => CharacterAppearanceA = CharacterAppearanceA( val aa: Int => CharacterAppearanceA = CharacterAppearanceA(
obj.avatar.basic, tempAvatarInfo,
CommonFieldData( CommonFieldData(
obj.Faction, obj.Faction,
bops = obj.spectator, bops = obj.bops,
alt_model_flag, alt_model_flag,
v1 = false, v1 = false,
None, None,
@ -106,7 +112,7 @@ object AvatarConverter {
unk7 = false, unk7 = false,
on_zipline = None on_zipline = None
) )
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars) CharacterAppearanceData(aa, ab, avatar.decoration.ribbonBars)
} }
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = { def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {

View file

@ -110,6 +110,6 @@ class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() {
} }
def showBfrShield(obj: Vehicle): Boolean = { def showBfrShield(obj: Vehicle): Boolean = {
obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0
} }
} }

View file

@ -110,6 +110,6 @@ class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() {
} }
def showBfrShield(obj: Vehicle): Boolean = { def showBfrShield(obj: Vehicle): Boolean = {
obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0
} }
} }

View file

@ -324,7 +324,7 @@ object EffectTarget {
def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean = def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
target match { target match {
case v: Vehicle case v: Vehicle
if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) => if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) && v.Definition != GlobalDefinitions.mosquito =>
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val pos = v.Position val pos = v.Position
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f) lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
@ -332,10 +332,10 @@ object EffectTarget {
.collect { case t: Tool => now - t.LastDischarge } .collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L) .exists(_ < 2000L)
// from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60 // from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60
lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61 //lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61
lazy val isMoving = v.isMoving(test = 1d) lazy val isMoving = v.isMoving(test = 1d)
if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (v.Definition == GlobalDefinitions.mosquito) movingFast //else if (v.Definition == GlobalDefinitions.mosquito) movingFast
else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment) else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment)
case _ => case _ =>
false false

View file

@ -129,7 +129,7 @@ object GlobalDefinitionsBuilding {
hst.NoWarp += colossus_flight hst.NoWarp += colossus_flight
hst.NoWarp += peregrine_gunner hst.NoWarp += peregrine_gunner
hst.NoWarp += peregrine_flight hst.NoWarp += peregrine_flight
hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 6f) hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f)
warpgate.Name = "warpgate" warpgate.Name = "warpgate"
warpgate.UseRadius = 67.81070029f warpgate.UseRadius = 67.81070029f
@ -141,7 +141,7 @@ object GlobalDefinitionsBuilding {
warpgate_cavern.UseRadius = 19.72639434f warpgate_cavern.UseRadius = 19.72639434f
warpgate_cavern.SOIRadius = 41 warpgate_cavern.SOIRadius = 41
warpgate_cavern.VehicleAllowance = true warpgate_cavern.VehicleAllowance = true
warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 4.5f) warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f)
warpgate_small.Name = "warpgate_small" warpgate_small.Name = "warpgate_small"
warpgate_small.UseRadius = 69.03687655f warpgate_small.UseRadius = 69.03687655f

View file

@ -19,6 +19,7 @@ import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.etc.ExplodingRadialDegrade import net.psforever.objects.vital.etc.ExplodingRadialDegrade
import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.types.{ExoSuitType, Vector3} import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.util.Config
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -707,7 +708,7 @@ object GlobalDefinitionsMiscellaneous {
capture_terminal.Name = "capture_terminal" capture_terminal.Name = "capture_terminal"
capture_terminal.Damageable = false capture_terminal.Damageable = false
capture_terminal.Repairable = false capture_terminal.Repairable = false
capture_terminal.FacilityHackTime = 15.minutes capture_terminal.FacilityHackTime = Config.app.game.facilityHackTime
secondary_capture.Name = "secondary_capture" secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = false secondary_capture.Damageable = false

View file

@ -289,7 +289,7 @@ object GlobalDefinitionsProjectile {
chainblade_projectile.Damage1 = 0 chainblade_projectile.Damage1 = 0
chainblade_projectile.ProjectileDamageType = DamageType.Direct chainblade_projectile.ProjectileDamageType = DamageType.Direct
chainblade_projectile.InitialVelocity = 100 chainblade_projectile.InitialVelocity = 100
chainblade_projectile.Lifespan = .02f chainblade_projectile.Lifespan = .03f //.02f
ProjectileDefinition.CalculateDerivedFields(chainblade_projectile) ProjectileDefinition.CalculateDerivedFields(chainblade_projectile)
chainblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) chainblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff)
@ -601,7 +601,7 @@ object GlobalDefinitionsProjectile {
forceblade_projectile.Damage1 = 0 forceblade_projectile.Damage1 = 0
forceblade_projectile.ProjectileDamageType = DamageType.Direct forceblade_projectile.ProjectileDamageType = DamageType.Direct
forceblade_projectile.InitialVelocity = 100 forceblade_projectile.InitialVelocity = 100
forceblade_projectile.Lifespan = .02f forceblade_projectile.Lifespan = .03f //.02f
ProjectileDefinition.CalculateDerivedFields(forceblade_projectile) ProjectileDefinition.CalculateDerivedFields(forceblade_projectile)
forceblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) forceblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff)
@ -942,7 +942,7 @@ object GlobalDefinitionsProjectile {
katana_projectile.Damage1 = 0 katana_projectile.Damage1 = 0
katana_projectile.ProjectileDamageType = DamageType.Direct katana_projectile.ProjectileDamageType = DamageType.Direct
katana_projectile.InitialVelocity = 100 katana_projectile.InitialVelocity = 100
katana_projectile.Lifespan = .03f katana_projectile.Lifespan = .04f //.03f
ProjectileDefinition.CalculateDerivedFields(katana_projectile) ProjectileDefinition.CalculateDerivedFields(katana_projectile)
katana_projectileb.Name = "katana_projectileb" katana_projectileb.Name = "katana_projectileb"
@ -1088,7 +1088,7 @@ object GlobalDefinitionsProjectile {
magcutter_projectile.Damage1 = 0 magcutter_projectile.Damage1 = 0
magcutter_projectile.ProjectileDamageType = DamageType.Direct magcutter_projectile.ProjectileDamageType = DamageType.Direct
magcutter_projectile.InitialVelocity = 100 magcutter_projectile.InitialVelocity = 100
magcutter_projectile.Lifespan = .02f magcutter_projectile.Lifespan = .03f //.02f
ProjectileDefinition.CalculateDerivedFields(magcutter_projectile) ProjectileDefinition.CalculateDerivedFields(magcutter_projectile)
magcutter_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) magcutter_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff)
@ -2056,7 +2056,7 @@ object GlobalDefinitionsProjectile {
aphelion_plasma_rocket_projectile.DamageAtEdge = .1f aphelion_plasma_rocket_projectile.DamageAtEdge = .1f
aphelion_plasma_rocket_projectile.DamageRadius = 3f aphelion_plasma_rocket_projectile.DamageRadius = 3f
aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash
aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud //aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud
aphelion_plasma_rocket_projectile.InitialVelocity = 75 aphelion_plasma_rocket_projectile.InitialVelocity = 75
aphelion_plasma_rocket_projectile.Lifespan = 5f aphelion_plasma_rocket_projectile.Lifespan = 5f
ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile) ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile)
@ -2221,7 +2221,7 @@ object GlobalDefinitionsProjectile {
peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f
peregrine_particle_cannon_projectile.DamageRadius = 3f peregrine_particle_cannon_projectile.DamageRadius = 3f
peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash
peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud //peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud
peregrine_particle_cannon_projectile.InitialVelocity = 500 peregrine_particle_cannon_projectile.InitialVelocity = 500
peregrine_particle_cannon_projectile.Lifespan = .6f peregrine_particle_cannon_projectile.Lifespan = .6f
ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile) ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile)

View file

@ -46,7 +46,7 @@ object GlobalDefinitionsVehicle {
restriction = MaxOnly restriction = MaxOnly
} }
val controlSubsystem = List(VehicleSubsystemEntry.Controls) val controlSubsystem: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls)
fury.Name = "fury" fury.Name = "fury"
fury.MaxHealth = 650 fury.MaxHealth = 650
@ -83,6 +83,7 @@ object GlobalDefinitionsVehicle {
fury.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) fury.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350)))
fury.maxForwardSpeed = 90f fury.maxForwardSpeed = 90f
fury.mass = 32.1f fury.mass = 32.1f
fury.MapRevealId = 14
quadassault.Name = "quadassault" // Basilisk quadassault.Name = "quadassault" // Basilisk
quadassault.MaxHealth = 650 quadassault.MaxHealth = 650
@ -119,6 +120,7 @@ object GlobalDefinitionsVehicle {
quadassault.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadassault.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350)))
quadassault.maxForwardSpeed = 90f quadassault.maxForwardSpeed = 90f
quadassault.mass = 32.1f quadassault.mass = 32.1f
quadassault.MapRevealId = 30
quadstealth.Name = "quadstealth" // Wraith quadstealth.Name = "quadstealth" // Wraith
quadstealth.MaxHealth = 650 quadstealth.MaxHealth = 650
@ -155,6 +157,7 @@ object GlobalDefinitionsVehicle {
quadstealth.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadstealth.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350)))
quadstealth.maxForwardSpeed = 90f quadstealth.maxForwardSpeed = 90f
quadstealth.mass = 32.1f quadstealth.mass = 32.1f
quadstealth.MapRevealId = 15
two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser
two_man_assault_buggy.MaxHealth = 1250 two_man_assault_buggy.MaxHealth = 1250
@ -193,6 +196,7 @@ object GlobalDefinitionsVehicle {
two_man_assault_buggy.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.5f, 600))) two_man_assault_buggy.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.5f, 600)))
two_man_assault_buggy.maxForwardSpeed = 85f two_man_assault_buggy.maxForwardSpeed = 85f
two_man_assault_buggy.mass = 52.4f two_man_assault_buggy.mass = 52.4f
two_man_assault_buggy.MapRevealId = 1
skyguard.Name = "skyguard" skyguard.Name = "skyguard"
skyguard.MaxHealth = 1000 skyguard.MaxHealth = 1000
@ -232,6 +236,7 @@ object GlobalDefinitionsVehicle {
skyguard.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.4f, 600))) skyguard.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.4f, 600)))
skyguard.maxForwardSpeed = 90f skyguard.maxForwardSpeed = 90f
skyguard.mass = 78.9f skyguard.mass = 78.9f
skyguard.MapRevealId = 16
threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.Name = "threemanheavybuggy" // Marauder
threemanheavybuggy.MaxHealth = 1700 threemanheavybuggy.MaxHealth = 1700
@ -274,6 +279,7 @@ object GlobalDefinitionsVehicle {
threemanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) threemanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900)))
threemanheavybuggy.maxForwardSpeed = 80f threemanheavybuggy.maxForwardSpeed = 80f
threemanheavybuggy.mass = 96.3f threemanheavybuggy.mass = 96.3f
threemanheavybuggy.MapRevealId = 18
twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer
twomanheavybuggy.MaxHealth = 1800 twomanheavybuggy.MaxHealth = 1800
@ -313,6 +319,7 @@ object GlobalDefinitionsVehicle {
twomanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900)))
twomanheavybuggy.maxForwardSpeed = 80f twomanheavybuggy.maxForwardSpeed = 80f
twomanheavybuggy.mass = 83.2f twomanheavybuggy.mass = 83.2f
twomanhoverbuggy.MapRevealId = 20
twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher
twomanhoverbuggy.MaxHealth = 1600 twomanhoverbuggy.MaxHealth = 1600
@ -355,6 +362,7 @@ object GlobalDefinitionsVehicle {
twomanhoverbuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanhoverbuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900)))
twomanhoverbuggy.maxForwardSpeed = 85f twomanhoverbuggy.maxForwardSpeed = 85f
twomanhoverbuggy.mass = 55.5f twomanhoverbuggy.mass = 55.5f
twomanhoverbuggy.MapRevealId = 21
mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.Name = "mediumtransport" // Deliverer
mediumtransport.MaxHealth = 2500 mediumtransport.MaxHealth = 2500
@ -401,6 +409,7 @@ object GlobalDefinitionsVehicle {
mediumtransport.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) mediumtransport.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000)))
mediumtransport.maxForwardSpeed = 70f mediumtransport.maxForwardSpeed = 70f
mediumtransport.mass = 108.5f mediumtransport.mass = 108.5f
mediumtransport.MapRevealId = 10
battlewagon.Name = "battlewagon" // Raider battlewagon.Name = "battlewagon" // Raider
battlewagon.MaxHealth = 2500 battlewagon.MaxHealth = 2500
@ -450,6 +459,7 @@ object GlobalDefinitionsVehicle {
battlewagon.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) //inherited from mediumtransport battlewagon.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) //inherited from mediumtransport
battlewagon.maxForwardSpeed = 65f battlewagon.maxForwardSpeed = 65f
battlewagon.mass = 108.5f battlewagon.mass = 108.5f
battlewagon.MapRevealId = 10
thunderer.Name = "thunderer" thunderer.Name = "thunderer"
thunderer.MaxHealth = 2500 thunderer.MaxHealth = 2500
@ -496,6 +506,7 @@ object GlobalDefinitionsVehicle {
thunderer.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) thunderer.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000)))
thunderer.maxForwardSpeed = 65f thunderer.maxForwardSpeed = 65f
thunderer.mass = 108.5f thunderer.mass = 108.5f
thunderer.MapRevealId = 29
aurora.Name = "aurora" aurora.Name = "aurora"
aurora.MaxHealth = 2500 aurora.MaxHealth = 2500
@ -542,6 +553,7 @@ object GlobalDefinitionsVehicle {
aurora.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) aurora.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000)))
aurora.maxForwardSpeed = 65f aurora.maxForwardSpeed = 65f
aurora.mass = 108.5f aurora.mass = 108.5f
aurora.MapRevealId = 28
apc_tr.Name = "apc_tr" // Juggernaut apc_tr.Name = "apc_tr" // Juggernaut
apc_tr.MaxHealth = 6000 apc_tr.MaxHealth = 6000
@ -612,6 +624,7 @@ object GlobalDefinitionsVehicle {
apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000)))
apc_tr.maxForwardSpeed = 60f apc_tr.maxForwardSpeed = 60f
apc_tr.mass = 128.4f apc_tr.mass = 128.4f
apc_tr.MapRevealId = 32
apc_nc.Name = "apc_nc" // Vindicator apc_nc.Name = "apc_nc" // Vindicator
apc_nc.MaxHealth = 6000 apc_nc.MaxHealth = 6000
@ -682,6 +695,7 @@ object GlobalDefinitionsVehicle {
apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000)))
apc_nc.maxForwardSpeed = 60f apc_nc.maxForwardSpeed = 60f
apc_nc.mass = 128.4f apc_nc.mass = 128.4f
apc_nc.MapRevealId = 33
apc_vs.Name = "apc_vs" // Leviathan apc_vs.Name = "apc_vs" // Leviathan
apc_vs.MaxHealth = 6000 apc_vs.MaxHealth = 6000
@ -752,6 +766,7 @@ object GlobalDefinitionsVehicle {
apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000)))
apc_vs.maxForwardSpeed = 60f apc_vs.maxForwardSpeed = 60f
apc_vs.mass = 128.4f apc_vs.mass = 128.4f
apc_vs.MapRevealId = 41
lightning.Name = "lightning" lightning.Name = "lightning"
lightning.MaxHealth = 2000 lightning.MaxHealth = 2000
@ -790,6 +805,7 @@ object GlobalDefinitionsVehicle {
lightning.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 750))) lightning.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 750)))
lightning.maxForwardSpeed = 74f lightning.maxForwardSpeed = 74f
lightning.mass = 100.2f lightning.mass = 100.2f
lightning.MapRevealId = 7
prowler.Name = "prowler" prowler.Name = "prowler"
prowler.MaxHealth = 4800 prowler.MaxHealth = 4800
@ -833,6 +849,7 @@ object GlobalDefinitionsVehicle {
prowler.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) prowler.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500)))
prowler.maxForwardSpeed = 57f prowler.maxForwardSpeed = 57f
prowler.mass = 510.5f prowler.mass = 510.5f
prowler.MapRevealId = 13
vanguard.Name = "vanguard" vanguard.Name = "vanguard"
vanguard.MaxHealth = 5400 vanguard.MaxHealth = 5400
@ -872,6 +889,7 @@ object GlobalDefinitionsVehicle {
vanguard.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 100), (30f, 250), (32.5f, 600))) vanguard.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 100), (30f, 250), (32.5f, 600)))
vanguard.maxForwardSpeed = 60f vanguard.maxForwardSpeed = 60f
vanguard.mass = 460.4f vanguard.mass = 460.4f
vanguard.MapRevealId = 22
magrider.Name = "magrider" magrider.Name = "magrider"
magrider.MaxHealth = 4200 magrider.MaxHealth = 4200
@ -913,6 +931,7 @@ object GlobalDefinitionsVehicle {
magrider.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) magrider.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500)))
magrider.maxForwardSpeed = 65f magrider.maxForwardSpeed = 65f
magrider.mass = 75.3f magrider.mass = 75.3f
magrider.MapRevealId = 8
val utilityConverter = new UtilityVehicleConverter val utilityConverter = new UtilityVehicleConverter
ant.Name = "ant" ant.Name = "ant"
@ -952,6 +971,7 @@ object GlobalDefinitionsVehicle {
ant.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 500), (13f, 750))) ant.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 500), (13f, 750)))
ant.maxForwardSpeed = 65f ant.maxForwardSpeed = 65f
ant.mass = 80.5f ant.mass = 80.5f
ant.MapRevealId = 1
ams.Name = "ams" ams.Name = "ams"
ams.MaxHealth = 3000 ams.MaxHealth = 3000
@ -995,6 +1015,7 @@ object GlobalDefinitionsVehicle {
ams.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 805), (13f, 3000))) ams.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 805), (13f, 3000)))
ams.maxForwardSpeed = 70f ams.maxForwardSpeed = 70f
ams.mass = 136.8f ams.mass = 136.8f
ams.MapRevealId = 2
val variantConverter = new VariantVehicleConverter val variantConverter = new VariantVehicleConverter
router.Name = "router" router.Name = "router"
@ -1039,6 +1060,7 @@ object GlobalDefinitionsVehicle {
router.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) router.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900)))
router.maxForwardSpeed = 60f router.maxForwardSpeed = 60f
router.mass = 60f router.mass = 60f
router.MapRevealId = 25
switchblade.Name = "switchblade" switchblade.Name = "switchblade"
switchblade.MaxHealth = 1750 switchblade.MaxHealth = 1750
@ -1085,6 +1107,7 @@ object GlobalDefinitionsVehicle {
switchblade.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 800))) switchblade.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 800)))
switchblade.maxForwardSpeed = 80f switchblade.maxForwardSpeed = 80f
switchblade.mass = 63.9f switchblade.mass = 63.9f
switchblade.MapRevealId = 26
flail.Name = "flail" flail.Name = "flail"
flail.MaxHealth = 2400 flail.MaxHealth = 2400
@ -1127,6 +1150,7 @@ object GlobalDefinitionsVehicle {
flail.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) flail.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900)))
flail.maxForwardSpeed = 55f flail.maxForwardSpeed = 55f
flail.mass = 73.5f flail.mass = 73.5f
flail.MapRevealId = 24
} }
/** /**
@ -1138,7 +1162,7 @@ object GlobalDefinitionsVehicle {
bailable = true bailable = true
} }
val flightSubsystems = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) val flightSubsystems: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection)
val variantConverter = new VariantVehicleConverter val variantConverter = new VariantVehicleConverter
mosquito.Name = "mosquito" mosquito.Name = "mosquito"
@ -1179,6 +1203,7 @@ object GlobalDefinitionsVehicle {
mosquito.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) mosquito.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100)))
mosquito.maxForwardSpeed = 120f mosquito.maxForwardSpeed = 120f
mosquito.mass = 53.6f mosquito.mass = 53.6f
mosquito.MapRevealId = 11
lightgunship.Name = "lightgunship" // Reaver lightgunship.Name = "lightgunship" // Reaver
lightgunship.MaxHealth = 855 // Temporary - Correct Reaver Health from pre-"Coder Madness 2" Event lightgunship.MaxHealth = 855 // Temporary - Correct Reaver Health from pre-"Coder Madness 2" Event
@ -1219,6 +1244,7 @@ object GlobalDefinitionsVehicle {
lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125)))
lightgunship.maxForwardSpeed = 104f lightgunship.maxForwardSpeed = 104f
lightgunship.mass = 51.1f lightgunship.mass = 51.1f
lightgunship.MapRevealId = 6
wasp.Name = "wasp" wasp.Name = "wasp"
wasp.MaxHealth = 515 wasp.MaxHealth = 515
@ -1257,6 +1283,7 @@ object GlobalDefinitionsVehicle {
wasp.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) //mosquito numbers wasp.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) //mosquito numbers
wasp.maxForwardSpeed = 120f wasp.maxForwardSpeed = 120f
wasp.mass = 53.6f wasp.mass = 53.6f
wasp.MapRevealId = 11
liberator.Name = "liberator" liberator.Name = "liberator"
liberator.MaxHealth = 2500 liberator.MaxHealth = 2500
@ -1305,6 +1332,7 @@ object GlobalDefinitionsVehicle {
liberator.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) liberator.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125)))
liberator.maxForwardSpeed = 90f liberator.maxForwardSpeed = 90f
liberator.mass = 82f liberator.mass = 82f
liberator.MapRevealId = 5
vulture.Name = "vulture" vulture.Name = "vulture"
vulture.MaxHealth = 2500 vulture.MaxHealth = 2500
@ -1354,6 +1382,7 @@ object GlobalDefinitionsVehicle {
vulture.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) vulture.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125)))
vulture.maxForwardSpeed = 97f vulture.maxForwardSpeed = 97f
vulture.mass = 82f vulture.mass = 82f
vulture.MapRevealId = 5
dropship.Name = "dropship" // Galaxy dropship.Name = "dropship" // Galaxy
dropship.MaxHealth = 5000 dropship.MaxHealth = 5000
@ -1430,6 +1459,7 @@ object GlobalDefinitionsVehicle {
dropship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) dropship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000)))
dropship.maxForwardSpeed = 80f dropship.maxForwardSpeed = 80f
dropship.mass = 133f dropship.mass = 133f
dropship.MapRevealId = 4
galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.Name = "galaxy_gunship"
galaxy_gunship.MaxHealth = 6000 galaxy_gunship.MaxHealth = 6000
@ -1490,6 +1520,7 @@ object GlobalDefinitionsVehicle {
galaxy_gunship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) galaxy_gunship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000)))
galaxy_gunship.maxForwardSpeed = 85f galaxy_gunship.maxForwardSpeed = 85f
galaxy_gunship.mass = 133f galaxy_gunship.mass = 133f
galaxy_gunship.MapRevealId = 31
lodestar.Name = "lodestar" lodestar.Name = "lodestar"
lodestar.MaxHealth = 5000 lodestar.MaxHealth = 5000
@ -1542,6 +1573,7 @@ object GlobalDefinitionsVehicle {
lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000)))
lodestar.maxForwardSpeed = 80f lodestar.maxForwardSpeed = 80f
lodestar.mass = 128.2f lodestar.mass = 128.2f
lodestar.MapRevealId = 34
phantasm.Name = "phantasm" phantasm.Name = "phantasm"
phantasm.MaxHealth = 2500 phantasm.MaxHealth = 2500
@ -1587,6 +1619,7 @@ object GlobalDefinitionsVehicle {
phantasm.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) phantasm.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125)))
phantasm.maxForwardSpeed = 140f phantasm.maxForwardSpeed = 140f
phantasm.mass = 100f phantasm.mass = 100f
phantasm.MapRevealId = 35
droppod.Name = "droppod" droppod.Name = "droppod"
droppod.MaxHealth = 20000 droppod.MaxHealth = 20000
@ -1651,20 +1684,20 @@ object GlobalDefinitionsVehicle {
bailable = true bailable = true
} }
val normalSeat = new SeatDefinition() val normalSeat = new SeatDefinition()
val bfrSubsystems = List( val bfrSubsystems: List[VehicleSubsystemEntry] = List(
VehicleSubsystemEntry.BattleframeMovementServos, // VehicleSubsystemEntry.BattleframeMovementServos,
VehicleSubsystemEntry.BattleframeSensorArray, // VehicleSubsystemEntry.BattleframeSensorArray,
VehicleSubsystemEntry.BattleframeShieldGenerator, VehicleSubsystemEntry.BattleframeShieldGenerator//,
VehicleSubsystemEntry.BattleframeTrunk // VehicleSubsystemEntry.BattleframeTrunk
) )
val bfrGunnerSubsystems = List( val bfrGunnerSubsystems: List[VehicleSubsystemEntry] = List(
VehicleSubsystemEntry.BattleframeLeftArm, VehicleSubsystemEntry.BattleframeLeftArm,
VehicleSubsystemEntry.BattleframeRightArm, VehicleSubsystemEntry.BattleframeRightArm//,
VehicleSubsystemEntry.BattleframeLeftWeapon, // VehicleSubsystemEntry.BattleframeLeftWeapon,
VehicleSubsystemEntry.BattleframeRightWeapon, // VehicleSubsystemEntry.BattleframeRightWeapon,
VehicleSubsystemEntry.BattleframeGunnerWeapon // VehicleSubsystemEntry.BattleframeGunnerWeapon
) ++ bfrSubsystems ) ++ bfrSubsystems
val bfrFlightSubsystems = List( val bfrFlightSubsystems: List[VehicleSubsystemEntry] = List(
VehicleSubsystemEntry.BattleframeFlightLeftArm, VehicleSubsystemEntry.BattleframeFlightLeftArm,
VehicleSubsystemEntry.BattleframeFlightRightArm, VehicleSubsystemEntry.BattleframeFlightRightArm,
VehicleSubsystemEntry.BattleframeFlightLeftWeapon, VehicleSubsystemEntry.BattleframeFlightLeftWeapon,
@ -1702,7 +1735,7 @@ object GlobalDefinitionsVehicle {
aphelion_gunner.AutoPilotSpeeds = (5, 1) aphelion_gunner.AutoPilotSpeeds = (5, 1)
aphelion_gunner.Packet = battleFrameConverter aphelion_gunner.Packet = battleFrameConverter
aphelion_gunner.DestroyedModel = None aphelion_gunner.DestroyedModel = None
aphelion_gunner.destructionDelay = Some(4000L) //aphelion_gunner.destructionDelay = Some(4000L)
aphelion_gunner.JackingDuration = Array(0, 62, 60, 30) aphelion_gunner.JackingDuration = Array(0, 62, 60, 30)
aphelion_gunner.RadiationShielding = 0.5f aphelion_gunner.RadiationShielding = 0.5f
aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr
@ -1724,6 +1757,7 @@ object GlobalDefinitionsVehicle {
aphelion_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
aphelion_gunner.maxForwardSpeed = 17 aphelion_gunner.maxForwardSpeed = 17
aphelion_gunner.mass = 615.1f aphelion_gunner.mass = 615.1f
aphelion_gunner.MapRevealId = 44
colossus_gunner.Name = "colossus_gunner" colossus_gunner.Name = "colossus_gunner"
colossus_gunner.MaxHealth = 4500 colossus_gunner.MaxHealth = 4500
@ -1753,7 +1787,7 @@ object GlobalDefinitionsVehicle {
colossus_gunner.AutoPilotSpeeds = (5, 1) colossus_gunner.AutoPilotSpeeds = (5, 1)
colossus_gunner.Packet = battleFrameConverter colossus_gunner.Packet = battleFrameConverter
colossus_gunner.DestroyedModel = None colossus_gunner.DestroyedModel = None
colossus_gunner.destructionDelay = Some(4000L) //colossus_gunner.destructionDelay = Some(4000L)
colossus_gunner.JackingDuration = Array(0, 62, 60, 30) colossus_gunner.JackingDuration = Array(0, 62, 60, 30)
colossus_gunner.RadiationShielding = 0.5f colossus_gunner.RadiationShielding = 0.5f
colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr
@ -1775,6 +1809,7 @@ object GlobalDefinitionsVehicle {
colossus_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
colossus_gunner.maxForwardSpeed = 17 colossus_gunner.maxForwardSpeed = 17
colossus_gunner.mass = 709.7f colossus_gunner.mass = 709.7f
colossus_gunner.MapRevealId = 48
peregrine_gunner.Name = "peregrine_gunner" peregrine_gunner.Name = "peregrine_gunner"
peregrine_gunner.MaxHealth = 4500 peregrine_gunner.MaxHealth = 4500
@ -1804,7 +1839,7 @@ object GlobalDefinitionsVehicle {
peregrine_gunner.AutoPilotSpeeds = (5, 1) peregrine_gunner.AutoPilotSpeeds = (5, 1)
peregrine_gunner.Packet = battleFrameConverter peregrine_gunner.Packet = battleFrameConverter
peregrine_gunner.DestroyedModel = None peregrine_gunner.DestroyedModel = None
peregrine_gunner.destructionDelay = Some(4000L) //peregrine_gunner.destructionDelay = Some(4000L)
peregrine_gunner.JackingDuration = Array(0, 62, 60, 30) peregrine_gunner.JackingDuration = Array(0, 62, 60, 30)
peregrine_gunner.RadiationShielding = 0.5f peregrine_gunner.RadiationShielding = 0.5f
peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr
@ -1826,6 +1861,7 @@ object GlobalDefinitionsVehicle {
peregrine_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
peregrine_gunner.maxForwardSpeed = 17 peregrine_gunner.maxForwardSpeed = 17
peregrine_gunner.mass = 713f peregrine_gunner.mass = 713f
peregrine_gunner.MapRevealId = 52
val battleFrameFlightConverter = new BattleFrameFlightConverter val battleFrameFlightConverter = new BattleFrameFlightConverter
aphelion_flight.Name = "aphelion_flight" aphelion_flight.Name = "aphelion_flight"
@ -1881,6 +1917,7 @@ object GlobalDefinitionsVehicle {
aphelion_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
aphelion_flight.maxForwardSpeed = 35 aphelion_flight.maxForwardSpeed = 35
aphelion_flight.mass = 615.1f aphelion_flight.mass = 615.1f
aphelion_flight.MapRevealId = 45
colossus_flight.Name = "colossus_flight" colossus_flight.Name = "colossus_flight"
colossus_flight.MaxHealth = 3500 colossus_flight.MaxHealth = 3500
@ -1935,6 +1972,7 @@ object GlobalDefinitionsVehicle {
colossus_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
colossus_flight.maxForwardSpeed = 34 colossus_flight.maxForwardSpeed = 34
colossus_flight.mass = 709.7f colossus_flight.mass = 709.7f
colossus_flight.MapRevealId = 49
peregrine_flight.Name = "peregrine_flight" peregrine_flight.Name = "peregrine_flight"
peregrine_flight.MaxHealth = 3500 peregrine_flight.MaxHealth = 3500
@ -1989,5 +2027,6 @@ object GlobalDefinitionsVehicle {
peregrine_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32)))
peregrine_flight.maxForwardSpeed = 35 peregrine_flight.maxForwardSpeed = 35
peregrine_flight.mass = 713f peregrine_flight.mass = 713f
peregrine_flight.MapRevealId = 51
} }
} }

View file

@ -2,6 +2,7 @@
package net.psforever.objects.serverobject.damage package net.psforever.objects.serverobject.damage
import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
@ -199,9 +200,12 @@ object DamageableEntity {
val tguid = target.GUID val tguid = target.GUID
val attribution = attributionTo(cause, target.Zone) val attribution = attributionTo(cause, target.Zone)
zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health)) zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health))
if (target.isInstanceOf[SpawnTube]) {}//do nothing to prevent issue #1057
else {
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zoneId, zoneId,
AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position) AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position)
) )
} }
} }
}

View file

@ -82,7 +82,7 @@ object EnvironmentAttribute {
/** only interact with living player characters or vehicles */ /** only interact with living player characters or vehicles */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj)
def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 4f
} }
/** /**

View file

@ -192,6 +192,10 @@ class GeneratorControl(gen: Generator)
if(newHealth == target.Definition.MaxHealth) { if(newHealth == target.Definition.MaxHealth) {
stopAutoRepair() stopAutoRepair()
} }
if(gen.Condition == PlanetSideGeneratorState.Critical && newHealth > (target.MaxHealth / 2)) {
gen.Condition = PlanetSideGeneratorState.Normal
GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Normal))
}
newHealth newHealth
} }

View file

@ -1,11 +1,13 @@
//Copyright (c) 2020 PSForever //Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.repair package net.psforever.objects.serverobject.repair
import net.psforever.objects.avatar.PlayerControl.sendResponse
import net.psforever.objects.ce.DeployableCategory
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.RepairFromEquipment import net.psforever.objects.vital.RepairFromEquipment
import net.psforever.objects.{Player, Tool} import net.psforever.objects.{Player, Tool}
import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} import net.psforever.packet.game.{ChatMsg, InventoryStateMessage, RepairMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -113,6 +115,27 @@ trait RepairableEntity extends Repairable {
RepairMessage(target.GUID, updatedHealth * 100 / definition.MaxHealth) RepairMessage(target.GUID, updatedHealth * 100 / definition.MaxHealth)
) )
) )
//if vehicle and vehicle is owned by another player, send repair chat message to the vehicle's owner
if (target.Zone.Vehicles.exists(_.GUID == target.GUID)) {
val vehicle = target.Zone.Vehicles.filter(_.GUID == target.GUID).head
val vehicleOwner = vehicle.OwnerName.getOrElse("someone")
val amount = updatedHealth - originalHealth
if (vehicleOwner != "someone" && vehicle.OwnerGuid.get != player.GUID && amount > 0) {
val repairMessageOther = s"@YourWasHealedMessage^@${vehicle.Definition.Name}~^repaired~^$amount~^armor~^${player.Name}~"
sendResponse(vehicle.Zone, vehicleOwner, ChatMsg(ChatMessageType.UNK_227, repairMessageOther))
}
}
//same check for field turret
if (target.Zone.DeployableList.exists(_.GUID == target.GUID)) {
val turret = target.Zone.DeployableList.filter(_.GUID == target.GUID).head
val turretOwner = turret.OwnerName.getOrElse("someone")
val amount = updatedHealth - originalHealth
if (turret.Definition.DeployCategory == DeployableCategory.FieldTurrets && turretOwner != "someone"
&& turret.OwnerGuid.get != player.GUID && amount > 0) {
val repairMessageOther = s"@YourWasHealedMessage^@${turret.Definition.Name}~^repaired~^$amount~^armor~^${player.Name}~"
sendResponse(turret.Zone, turretOwner, ChatMsg(ChatMessageType.UNK_227, repairMessageOther))
}
}
} }
protected def PerformRepairs(target: Repairable.Target, amount: Int): Int = { protected def PerformRepairs(target: Repairable.Target, amount: Int): Int = {

View file

@ -11,13 +11,14 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage}
import net.psforever.types._ import net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.util.Config
class Building( class Building(
private val name: String, private val name: String,
@ -236,6 +237,46 @@ class Building(
) )
} }
def densityLevelUpdateMessage(building: Building): DensityLevelUpdateMessage = {
if (building.PlayersInSOI.nonEmpty) {
val factionCounts: Map[PlanetSideEmpire.Value, Int] =
building.PlayersInSOI.groupBy(_.Faction).view.mapValues(_.size).toMap
val otherEmpireCounts: Map[PlanetSideEmpire.Value, Int] = PlanetSideEmpire.values.map {
faction =>
val otherCount = factionCounts.filterNot(_._1 == faction).values.sum
faction -> otherCount
}.toMap
val trAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.TR, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val ncAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NC, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val vsAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.VS, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val boAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NEUTRAL, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
DensityLevelUpdateMessage(Zone.Number, MapId, List(0, trAlert, 0, ncAlert, 0, vsAlert, 0, boAlert))
}
else { //nobody is in this SOI
DensityLevelUpdateMessage(Zone.Number, MapId, List(0, 0, 0, 0, 0, 0, 0, 0))
}
}
def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = { def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
val baseDownState = (NtuSource match { val baseDownState = (NtuSource match {
case Some(ntu) => ntu.NtuCapacitor < 1f case Some(ntu) => ntu.NtuCapacitor < 1f

View file

@ -41,13 +41,13 @@ trait FacilityHackParticipation extends ParticipationLogic {
.filterNot { case (_, (_, _, t)) => curr - t > hackTime } .filterNot { case (_, (_, _, t)) => curr - t > hackTime }
.partition { case (p, _) => uniqueList2.contains(p) } .partition { case (p, _) => uniqueList2.contains(p) }
} }
val newParticipaants = list val newParticipants = list
.filterNot { p => .filterNot { p =>
playerContribution.exists { case (u, _) => p.CharId == u } playerContribution.exists { case (u, _) => p.CharId == u }
} }
playerContribution = playerContribution =
vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++ vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++
newParticipaants.map { p => (p.CharId, (p, 1, curr)) } ++ newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++
missingParticipants missingParticipants
} }
} }

View file

@ -9,6 +9,7 @@ import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.hackable.Hackable
@ -26,15 +27,39 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]()
var lastEnemyCount: List[Player] = List.empty
var alertTimeMillis: Long = 0L
def TryUpdate(): Unit = { def TryUpdate(): Unit = {
val list = building.PlayersInSOI val list = building.PlayersInSOI
if (list.nonEmpty) {
updatePlayers(list) updatePlayers(list)
}
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastInfoRequest > 60000L) { if (now - lastInfoRequest > 60000L) {
updatePopulationOverTime(list, now, before = 900000L) updatePopulationOverTime(list, now, before = 900000L)
updateHotSpotInfoOverTime() updateHotSpotInfoOverTime()
updateTime(now) updateTime(now)
} }
val enemies = list.filter(p => p.Faction != building.Faction) ++
building.Zone.blockMap.sector(building).corpseList
.filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius)
//alert defenders (actually goes to all clients) of population change for base alerts
//straight away if higher alert, delay if pop decreases enough to lower alert
if ((enemies.length >= Config.app.game.alert.yellow && lastEnemyCount.length < Config.app.game.alert.yellow) ||
(enemies.length >= Config.app.game.alert.orange && lastEnemyCount.length < Config.app.game.alert.orange) ||
(enemies.length >= Config.app.game.alert.red && lastEnemyCount.length < Config.app.game.alert.red) ||
(enemies.length < Config.app.game.alert.yellow && lastEnemyCount.length >= Config.app.game.alert.yellow &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) ||
(enemies.length < Config.app.game.alert.orange && lastEnemyCount.length >= Config.app.game.alert.orange &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) ||
(enemies.length < Config.app.game.alert.red && lastEnemyCount.length >= Config.app.game.alert.red &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3))
{
building.Actor ! BuildingActor.DensityLevelUpdate(building)
alertTimeMillis = now
lastEnemyCount = enemies
}
building.CaptureTerminal building.CaptureTerminal
.map(_.HackedBy) .map(_.HackedBy)
.collect { .collect {
@ -123,7 +148,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
hackStart, hackStart,
completionTime, completionTime,
opposingFaction, opposingFaction,
contributionOpposing contributionVictor
) )
) )
//1) experience from killing opposingFaction across duration of hack //1) experience from killing opposingFaction across duration of hack
@ -249,11 +274,14 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
overallTimeMultiplier * overallTimeMultiplier *
Config.app.game.experience.cep.rate + competitionBonus Config.app.game.experience.cep.rate + competitionBonus
).toLong ).toLong
//8. reward participants //8. reward participants that are still in the zone
//Classically, only players in the SOI are rewarded, and the llu runner too
val hackerId = hacker.CharId val hackerId = hacker.CharId
val contributingPlayers = contributionVictor
.filter { case (player, _, _) => player.Zone.id == building.Zone.id }
.map { case (player, _, _) => player }
.toList
//terminal hacker (always cep) //terminal hacker (always cep)
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
ToDatabase.reportFacilityCapture( ToDatabase.reportFacilityCapture(
hackerId, hackerId,
zoneNumber, zoneNumber,
@ -264,7 +292,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep)) events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
} }
//bystanders (cep if squad leader, bep otherwise) //bystanders (cep if squad leader, bep otherwise)
playersInSoi contributingPlayers
.filterNot { _.CharId == hackerId } .filterNot { _.CharId == hackerId }
.foreach { player => .foreach { player =>
val charId = player.CharId val charId = player.CharId
@ -336,7 +364,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat
list list
.map { case (p, f, kills) => .map { case (p, f, kills) =>
val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius } val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) >= towerRadius }
(p, f, filteredKills) (p, f, filteredKills)
} }
.filter { case (_, _, kills) => kills.nonEmpty } .filter { case (_, _, kills) => kills.nonEmpty }

View file

@ -11,11 +11,14 @@ import net.psforever.util.Config
final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation { final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation {
def TryUpdate(): Unit = { def TryUpdate(): Unit = {
val list = building.PlayersInSOI val list = building.PlayersInSOI
updatePlayers(building.PlayersInSOI) if (list.nonEmpty) {
updatePlayers(list)
}
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastInfoRequest > 60000L) { if (now - lastInfoRequest > 60000L) {
updatePopulationOverTime(list, now, before = 300000L) updatePopulationOverTime(list, now, before = 300000L)
} }
lastInfoRequest = now
} }
def RewardFacilityCapture( def RewardFacilityCapture(

View file

@ -205,7 +205,8 @@ trait AutomatedTurretBehavior {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if ( if (
currentTargetToken.isEmpty && currentTargetToken.isEmpty &&
target.Faction != AutomatedTurretObject.Faction target.Faction != AutomatedTurretObject.Faction &&
now >= currentTargetLastShotTime
) { ) {
currentTargetLastShotTime = now currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position) currentTargetLocation = Some(target.Position)
@ -215,7 +216,12 @@ trait AutomatedTurretBehavior {
true true
} else if ( } else if (
currentTargetToken.contains(SourceUniqueness(target)) && currentTargetToken.contains(SourceUniqueness(target)) &&
now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) { now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)
) {
val escapeRange = autoStats.map(_.ranges.escape).getOrElse(400f)
val distSq = Vector3.DistanceSquared(target.Position, AutomatedTurretObject.Position)
val escapeSq = escapeRange * escapeRange
if (distSq <= escapeSq) {
currentTargetLastShotTime = now currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position) currentTargetLocation = Some(target.Position)
cancelSelfReportedAutoFire() cancelSelfReportedAutoFire()
@ -223,6 +229,9 @@ trait AutomatedTurretBehavior {
} else { } else {
false false
} }
} else {
false
}
} }
/** /**

View file

@ -18,7 +18,7 @@ sealed abstract class VehicleSubsystemConditionModifier(
) extends IntEnumEntry ) extends IntEnumEntry
object VehicleSubsystemConditionModifier extends IntEnum[VehicleSubsystemConditionModifier] { object VehicleSubsystemConditionModifier extends IntEnum[VehicleSubsystemConditionModifier] {
val values = findValues val values: IndexedSeq[VehicleSubsystemConditionModifier] = findValues
case object Off extends VehicleSubsystemConditionModifier(value = 1065353216, multiplier = 0f, addend = 0) case object Off extends VehicleSubsystemConditionModifier(value = 1065353216, multiplier = 0f, addend = 0)

View file

@ -134,7 +134,7 @@ class BfrControl(vehicle: Vehicle)
item.asInstanceOf[Tool], item.asInstanceOf[Tool],
dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition] dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition]
) )
case _ => ; //no dimorphic entry; place as-is case _ => () //no dimorphic entry; place as-is
} }
val guid0 = PlanetSideGUID(0) val guid0 = PlanetSideGUID(0)
//if the weapon arm is disabled, enable it for later (makes life easy) //if the weapon arm is disabled, enable it for later (makes life easy)
@ -173,7 +173,8 @@ class BfrControl(vehicle: Vehicle)
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = { override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
super.dismountCleanup(seatBeingDismounted, player) super.dismountCleanup(seatBeingDismounted, player)
if (!vehicle.Seats.values.exists(_.isOccupied)) { if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { vehicle
.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
case Some(subsys) => case Some(subsys) =>
if (vehicle.Shields > 0) { if (vehicle.Shields > 0) {
vehicleSubsystemMessages( vehicleSubsystemMessages(
@ -190,7 +191,7 @@ class BfrControl(vehicle: Vehicle)
} }
) )
} }
case _ => ; case _ => ()
} }
} }
} }
@ -203,7 +204,7 @@ class BfrControl(vehicle: Vehicle)
if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) =>
//if the shield is damaged, it does not turn on until the damaged is cleared //if the shield is damaged, it does not turn on until the damaged is cleared
vehicleSubsystemMessages(subsys.changedMessages(vehicle)) vehicleSubsystemMessages(subsys.changedMessages(vehicle))
case _ => ; case _ => ()
} }
} }
} }
@ -425,9 +426,9 @@ class BfrControl(vehicle: Vehicle)
zone.id, zone.id,
VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action) VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action)
) )
case _ => ; case _ => ()
} }
case _ => ; case _ => ()
} }
} }
} }
@ -602,7 +603,7 @@ class BfrControl(vehicle: Vehicle)
) )
) )
} }
case _ => ; case _ => ()
} }
} }
} }

View file

@ -58,20 +58,20 @@ class BfrFlightControl(vehicle: Vehicle)
case Some(drain) if localFlyingValue.isEmpty => case Some(drain) if localFlyingValue.isEmpty =>
//shields off //shields off
disableShield() disableShield()
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false)
vehicle.Shields -= drain vehicle.Shields -= drain
showShieldCharge() showShieldCharge()
case None if localFlyingValue.isEmpty => case None if localFlyingValue.isEmpty =>
//shields off //shields off
disableShield() disableShield()
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false)
case Some(drain) => case Some(drain) =>
vehicle.Shields -= drain vehicle.Shields -= drain
showShieldCharge() showShieldCharge()
case _ => ; case _ => ;
} }
} }
if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).get.Jammed) { if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).exists(_.Jammed)) {
} }
@ -79,7 +79,7 @@ class BfrFlightControl(vehicle: Vehicle)
if (flying.nonEmpty) { if (flying.nonEmpty) {
flying = None flying = None
vehicle.Flying = None vehicle.Flying = None
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = true vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = true)
if (vehicle.Shields > 0) { if (vehicle.Shields > 0) {
enableShield() enableShield()
} }

View file

@ -27,7 +27,7 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.interaction.WithWater import net.psforever.objects.vehicles.interaction.WithWater
import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.zones._ import net.psforever.objects.zones._
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._ import net.psforever.packet.game._
@ -39,6 +39,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Random
/** /**
* An `Actor` that handles messages being dispatched to a specific `Vehicle`.<br> * An `Actor` that handles messages being dispatched to a specific `Vehicle`.<br>
@ -140,6 +141,36 @@ class VehicleControl(vehicle: Vehicle)
}) => }) =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
case Mountable.TryDismount(user, seat_num, bailType)
if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true
case _ if Random.nextInt(10) == 1 => false
case _ => true }) =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
case Mountable.TryDismount(user, seat_num, bailType)
if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true
case _ if Random.nextInt(5) == 1 => false
case _ => true }) =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
case Mountable.TryDismount(user, seat_num, bailType)
if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
case _ if Random.nextInt(4) == 1 => false
case _ => true }) =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
case Mountable.TryDismount(user, seat_num, bailType) case Mountable.TryDismount(user, seat_num, bailType)
if vehicle.DeploymentState == DriveState.AutoPilot => if vehicle.DeploymentState == DriveState.AutoPilot =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))

View file

@ -0,0 +1,14 @@
// Copyright (c) 2025 PSForever
package net.psforever.objects.vehicles.interaction
import net.psforever.objects.ce.TriggerTest
import net.psforever.objects.ExplosiveDeployable
import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.zones.Zone
case object TriggerOnVehicleRule
extends TriggerTest {
def test(g: VolumetricGeometry, obj: ExplosiveDeployable, radius: Float): Boolean = {
Zone.distanceCheck(g, obj, radius * radius)
}
}

View file

@ -4,7 +4,7 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource} import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason} import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@ -82,6 +82,9 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity extends GeneralActivity
final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource)
extends GeneralActivity
sealed trait VehicleMountChange extends GeneralActivity { sealed trait VehicleMountChange extends GeneralActivity {
def vehicle: VehicleSource def vehicle: VehicleSource
def zoneNumber: Int def zoneNumber: Int
@ -248,7 +251,7 @@ trait InGameHistory {
*/ */
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = { def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
action match { action match {
case Some(act: VehicleDismountActivity) => case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty =>
history history
.findLast(_.isInstanceOf[VehicleMountActivity]) .findLast(_.isInstanceOf[VehicleMountActivity])
.collect { .collect {
@ -259,6 +262,8 @@ trait InGameHistory {
history = history :+ act history = history :+ act
None None
} }
case Some(act: VehicleDismountActivity) =>
history = history :+ act
case Some(act: VehicleCargoDismountActivity) => case Some(act: VehicleCargoDismountActivity) =>
history history
.findLast(_.isInstanceOf[VehicleCargoMountActivity]) .findLast(_.isInstanceOf[VehicleCargoMountActivity])

View file

@ -3,6 +3,8 @@ package net.psforever.objects.vital.damage
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import scala.annotation.unused
/** /**
* A series of methods for extraction of the base damage against a given target type * A series of methods for extraction of the base damage against a given target type
* as well as incorporating damage modifiers from the other aspects of the interaction. * as well as incorporating damage modifiers from the other aspects of the interaction.
@ -11,7 +13,7 @@ object DamageCalculations {
type Selector = DamageProfile => Int type Selector = DamageProfile => Int
//raw damage selectors //raw damage selectors
def AgainstNothing(profile : DamageProfile) : Int = 0 def AgainstNothing(@unused profile : DamageProfile) : Int = 0
def AgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0 def AgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0

View file

@ -354,7 +354,7 @@ object ResolutionCalculations {
{ {
data.cause.source.DamageToBattleframeOnly || data.cause.source.DamageToBattleframeOnly ||
data.cause.source.DamageToVehicleOnly || data.cause.source.DamageToVehicleOnly ||
!obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled || !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) ||
obj.Shields == 0 obj.Shields == 0
} }
) )

View file

@ -375,7 +375,8 @@ case object MapInfo extends StringEnum[MapInfo] {
Pool(EnvironmentAttribute.Water, 192.32812f, 1966.1562f, 1252.7344f, 1889.8047f, 1148.5312f), //top, northern pool Pool(EnvironmentAttribute.Water, 192.32812f, 1966.1562f, 1252.7344f, 1889.8047f, 1148.5312f), //top, northern pool
Pool(EnvironmentAttribute.Water, 191.65625f, 1869.1484f, 1195.6406f, 1743.8125f, 1050.7344f), //middle, northern pool Pool(EnvironmentAttribute.Water, 191.65625f, 1869.1484f, 1195.6406f, 1743.8125f, 1050.7344f), //middle, northern pool
Pool(EnvironmentAttribute.Water, 183.98438f, 914.33594f, 1369.5f, 626.03906f, 666.3047f), //upper southern pools Pool(EnvironmentAttribute.Water, 183.98438f, 914.33594f, 1369.5f, 626.03906f, 666.3047f), //upper southern pools
Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f) //lowest southern pool Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f), //lowest southern pool
SeaLevel(EnvironmentAttribute.Death, 10)
) )
) )
@ -394,7 +395,10 @@ case object MapInfo extends StringEnum[MapInfo] {
checksum = 3797992164L, checksum = 3797992164L,
scale = MapScale.Dim2048, scale = MapScale.Dim2048,
hotSpotSpan = 80, hotSpotSpan = 80,
environment = List(Pool(EnvironmentAttribute.Death, DeepSurface(51.414f, 2048, 2048, 0, 0))) environment = List(
Pool(EnvironmentAttribute.Death, DeepSurface(51.414f, 2048, 2048, 0, 0)),
SeaLevel(EnvironmentAttribute.Death, 10)
)
) )
case object Ugd05 case object Ugd05
@ -451,7 +455,7 @@ case object MapInfo extends StringEnum[MapInfo] {
Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 4096f, 1227f, 2900f), //east Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 4096f, 1227f, 2900f), //east
Pool(EnvironmentAttribute.Water, 3.5f, 1227f, 4096f, 0f, 2000f), //southeast Pool(EnvironmentAttribute.Water, 3.5f, 1227f, 4096f, 0f, 2000f), //southeast
Pool(EnvironmentAttribute.Water, 3.5f, 1128f, 2000f, 0f, 0f), //southwest Pool(EnvironmentAttribute.Water, 3.5f, 1128f, 2000f, 0f, 0f), //southwest
Pool(EnvironmentAttribute.Death, 0.5f, 2867f, 2900f, 1128f, 1228f) //central, kill Pool(EnvironmentAttribute.Death, 0.5f, 2867f, 2900f, 1128f, 1228f), //central, kill
) ++ MapEnvironment.dim4096MapEdgeKillPlanes ) ++ MapEnvironment.dim4096MapEdgeKillPlanes
) )
@ -708,7 +712,7 @@ object MapEnvironment {
p.Name, p.Name,
AvatarAction.SendResponseTargeted( AvatarAction.SendResponseTargeted(
Service.defaultPlayerGUID, Service.defaultPlayerGUID,
ChatMsg(ChatMessageType.CMT_QUIT, false, "", warning, None) ChatMsg(ChatMessageType.CMT_QUIT, warning)
) )
) )
case _ => ; case _ => ;

View file

@ -55,9 +55,16 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor {
sois.foreach { sois.foreach {
case (facility, radius) => case (facility, radius) =>
val facilityXY = facility.Position.xy val facilityXY = facility.Position.xy
facility.PlayersInSOI = zone.blockMap.sector(facility) val playersOnFoot = zone.blockMap.sector(facility)
.livePlayerList .livePlayerList
.filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius) .filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius)
val vehicleOccupants = zone.blockMap.sector(facility)
.vehicleList
.filter(v => Vector3.DistanceSquared(facilityXY, v.Position.xy) < radius)
.flatMap(_.Seats.values.flatMap(_.occupants))
facility.PlayersInSOI = playersOnFoot ++ vehicleOccupants
} }
populateTick.cancel() populateTick.cancel()
populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate()) populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate())

View file

@ -182,6 +182,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/ */
private var vehicleEvents: ActorRef = Default.Actor private var vehicleEvents: ActorRef = Default.Actor
/**
* Is any player permitted to engage in weapons discharge in this zone?
*/
private var liveFireAllowed: mutable.HashMap[PlanetSideEmpire.Value, Boolean] =
mutable.HashMap.from(PlanetSideEmpire.values.map { f => (f, true) })
/** /**
* When the zone has completed initializing, fulfill this promise. * When the zone has completed initializing, fulfill this promise.
* @see `init(ActorContext)` * @see `init(ActorContext)`
@ -593,6 +599,46 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
vehicleEvents = bus vehicleEvents = bus
VehicleEvents VehicleEvents
} }
def LiveFireAllowed(): Boolean = liveFireAllowed.exists { case (_, v) => v }
def LiveFireAllowed(faction: PlanetSideEmpire.Value): Boolean = liveFireAllowed.getOrElse(faction, false)
def UpdateLiveFireAllowed(state: Boolean): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = {
val output = liveFireAllowed.map { case (f, v) =>
(f, v == state, state)
}
output.foreach { case (f, _, v) =>
liveFireAllowed.update(f, v)
}
output.toList
}
def UpdateLiveFireAllowed(state: Boolean, faction: PlanetSideEmpire.Value): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = {
val output = liveFireAllowed.map { case (f, v) =>
if (f == faction) {
(f, v == state, state)
} else {
(f, false, v)
}
}
liveFireAllowed.update(faction, state)
output.toList
}
def UpdateLiveFireAllowed(state: Boolean, factions: Seq[PlanetSideEmpire.Value]): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = {
val output = liveFireAllowed.map { case (f, v) =>
if (factions.contains(f)) {
(f, v == state, state)
} else {
(f, false, v)
}
}
factions.foreach { f =>
liveFireAllowed.update(f, state)
}
output.toList
}
} }
/** /**
@ -1845,6 +1891,53 @@ object Zone {
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
} }
/**
* na
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
* @param sourcePosition a position that is used as the origin of the explosion
* @param radius idistance
* @param getTargetsFromSector get this list of entities from a sector
* @return a list of affected entities
*/
def findOrbitalStrikeTargets(
zone: Zone,
sourcePosition: Vector3,
radius: Float,
getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
getTargetsFromSector(zone.blockMap.sector(sourcePosition.xy, radius))
}
def getOrbitbalStrikeTargets(sector: SectorPopulation): List[PlanetSideServerObject with Vitality] = {
//collect all targets that can be damaged
//players
val playerTargets = sector.livePlayerList.filter { player => player.VehicleSeated.isEmpty && player.WhichSide == Sidedness.OutsideOf }
//vehicles
val vehicleTargets = sector.vehicleList.filterNot { _.Destroyed }
//deployables
val deployableTargets = sector.deployableList.filter { obj => !obj.Destroyed && obj.WhichSide == Sidedness.OutsideOf }
//amenities
val soiTargets = sector.amenityList.collect {
case amenity: Vitality if !amenity.Destroyed && amenity.WhichSide == Sidedness.OutsideOf && amenity.CanDamage => amenity }
//altogether ...
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
}
/**
* Check if targets returned from sector are within range of the imminent Orbital Strike
* @param p1 OS position
* @param p2 target position
* @param maxDistance radius of the Orbital Strike
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def orbitalStrikeDistanceCheck(p1: Vector3, p2: Vector3, maxDistance: Float): Boolean = {
val radius = maxDistance * maxDistance
Vector3.DistanceSquared(p1.xy, p2.xy) <= radius
}
/** /**
* na * na
* @param instigation what previous event happened, if any, that caused this explosion * @param instigation what previous event happened, if any, that caused this explosion
@ -1901,6 +1994,24 @@ object Zone {
distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance)
} }
/**
* Two game entities are considered "near" each other if they are within a certain distance of one another.
* A default function literal mainly used for `serverSideDamage`.
* Best used when one entity - `obj1` - is to be reused in tests against multiple other entities
* as it skips repeatedly calculating the volumetric geometry of the repeating entity.
* @see `ObjectDefinition.Geometry`
* @see `serverSideDamage`
* @param obj1 the geometric representation of a game entity
* @param obj2 a game entity
* @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near"
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def distanceCheck(obj1: VolumetricGeometry, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = {
distanceCheck(obj1, obj2.Definition.Geometry(obj2), maxDistance)
}
/** /**
* Two game entities are considered "near" each other if they are within a certain distance of one another. * Two game entities are considered "near" each other if they are within a certain distance of one another.
* @param g1 the geometric representation of a game entity * @param g1 the geometric representation of a game entity

View file

@ -313,7 +313,7 @@ object SectorGroup {
new SectorGroup( new SectorGroup(
rangeX, rangeX,
rangeY, rangeY,
sector.livePlayerList, sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction),
sector.corpseList, sector.corpseList,
sector.vehicleList, sector.vehicleList,
sector.equipmentOnGroundList, sector.equipmentOnGroundList,
@ -368,7 +368,7 @@ object SectorGroup {
new SectorGroup( new SectorGroup(
rangeX, rangeX,
rangeY, rangeY,
sector.livePlayerList, sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction),
sector.corpseList, sector.corpseList,
sector.vehicleList, sector.vehicleList,
sector.equipmentOnGroundList, sector.equipmentOnGroundList,
@ -382,7 +382,7 @@ object SectorGroup {
new SectorGroup( new SectorGroup(
rangeX, rangeX,
rangeY, rangeY,
sectors.flatMap { _.livePlayerList }.toList.distinct, sectors.flatMap { _.livePlayerList }.toList.distinct.filterNot(p => p.spectator || !p.allowInteraction),
sectors.flatMap { _.corpseList }.toList.distinct, sectors.flatMap { _.corpseList }.toList.distinct,
sectors.flatMap { _.vehicleList }.toList.distinct, sectors.flatMap { _.vehicleList }.toList.distinct,
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct, sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,

View file

@ -5,7 +5,7 @@ import akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity} import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource} import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -55,6 +55,11 @@ object KillContributions {
/** cached for empty collection returns; please do not add anything to it */ /** cached for empty collection returns; please do not add anything to it */
private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats] private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
/** cached for use with telepad deployable activities, from the perspective of the router */
private val routerKillAssist = RouterKillAssist(GlobalDefinitions.router.ObjectId)
/** cached for use with telepad deployable activities */
private val routerTelepadKillAssist = RouterKillAssist(GlobalDefinitions.router_telepad_deployable.ObjectId)
/** /**
* Primary landing point for calculating the rewards given for helping one player kill another player. * Primary landing point for calculating the rewards given for helping one player kill another player.
* Rewards in the form of "support experience points" are given * Rewards in the form of "support experience points" are given
@ -263,6 +268,7 @@ object KillContributions {
): mutable.LongMap[ContributionStats] = { ): mutable.LongMap[ContributionStats] = {
contributeWithRevivalActivity(history, existingParticipants) contributeWithRevivalActivity(history, existingParticipants)
contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants) contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants)
contributeWithRouterTelepadActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants) contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
@ -371,10 +377,17 @@ object KillContributions {
/* /*
collect the dismount activity of all vehicles from which this player is not the owner collect the dismount activity of all vehicles from which this player is not the owner
make certain all dismount activity can be paired with a mounting activity make certain all dismount activity can be paired with a mounting activity
certain other qualifications of the prior mounting must be met before the support bonus applies other qualifications of the prior mounting must be met before the support bonus applies
*/ */
val killerOpt = kill.info.adversarial
.map(_.attacker)
.collect { case p: PlayerSource => p }
val dismountActivity = history val dismountActivity = history
.collect { .collect {
/*
the player should not get credit from being the vehicle owner in matters of transportation
there are considerations of time and distance traveled before the kill as well
*/
case out: VehicleDismountActivity case out: VehicleDismountActivity
if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
} }
@ -382,7 +395,10 @@ object KillContributions {
case (in: VehicleMountActivity, out: VehicleDismountActivity) case (in: VehicleMountActivity, out: VehicleDismountActivity)
if in.vehicle.unique == out.vehicle.unique && if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.player.Faction && out.vehicle.Faction == out.player.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || { /*
considerations of time and distance transported before the kill
*/
({
val inTime = in.time val inTime = in.time
val outTime = out.time val outTime = out.time
out.player.progress.kills.exists { death => out.player.progress.kills.exists { death =>
@ -393,9 +409,7 @@ object KillContributions {
val sameZone = in.zoneNumber == out.zoneNumber val sameZone = in.zoneNumber == out.zoneNumber
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val distanceMoved = { val distanceMoved = {
val killLocation = kill.info.adversarial val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero)
.collect { adversarial => adversarial.attacker.Position.xy }
.getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.player.Position.xy) Vector3.DistanceSquared(killLocation, out.player.Position.xy)
} }
val timeSpent = out.time - in.time val timeSpent = out.time - in.time
@ -412,25 +426,20 @@ object KillContributions {
.groupBy { _.vehicle } .groupBy { _.vehicle }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position) val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val (equipmentUseContext, equipmentUseEvent) = mount.Definition match {
case v @ GlobalDefinitions.router =>
(RouterKillAssist(v.ObjectId), "router")
case v =>
(HotDropKillAssist(v.ObjectId, 0), "hotdrop")
}
val size = dismountsFromVehicle.size val size = dismountsFromVehicle.size
val time = dismountsFromVehicle.maxBy(_.time).time val time = dismountsFromVehicle.maxBy(_.time).time
List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner))
.foreach {
case (equipmentUseContext, equipmentUseEvent, eventOwner) =>
val weaponStat = Support.calculateSupportExperience( val weaponStat = Support.calculateSupportExperience(
equipmentUseEvent, equipmentUseEvent,
WeaponStats(equipmentUseContext, size, size, time, 1f) WeaponStats(equipmentUseContext, size, size, time, 1f)
) )
combineStatsInto( combineStatsInto(
out, out,
( (eventOwner.CharId, ContributionStats(eventOwner, Seq(weaponStat), size, size, size, time))
promotedOwner.CharId,
ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time)
)
) )
}
contributions.get(mount.unique).collect { contributions.get(mount.unique).collect {
case list => case list =>
val mountHistory = dismountsFromVehicle val mountHistory = dismountsFromVehicle
@ -554,6 +563,86 @@ object KillContributions {
} }
} }
/**
* Gather and reward use of a telepad deployable in performing a kill.
* There are two ways to account for telepad deployable use,
* i.e., traveling through s telepad deployable or using the internal telepad system of a Router:
* the user that places the telepad deployable unit,
* and the user that owns the Router.<br>
* na
* @param kill the in-game event that maintains information about the other player's death
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithRouterTelepadActivity(
kill: Kill,
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
/*
collect the use of all router telepads from which this player is not the owner (deployer) of the telepad
*/
val killer = kill.info.adversarial
.map(_.attacker)
.collect { case p: PlayerSource => p }
.getOrElse(PlayerSource.Nobody)
history
.collect {
case event: TelepadUseActivity if !event.player.Name.equals(event.telepad.OwnerName) =>
event
}
.groupBy(_.telepad.unique)
.flatMap {
case (_, telepadEvents) =>
val size = telepadEvents.size
val time = telepadEvents.maxBy(_.time).time
val firstEvent = telepadEvents.head
val telepadOwner = firstEvent.telepad.owner.asInstanceOf[PlayerSource]
val mount = firstEvent.router
contributions.get(mount.unique).collect {
case list =>
val mountHistory = telepadEvents
.flatMap { event =>
val eventTime = event.time
val startTime = eventTime - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
telepadEvents
.flatMap(_.router.owner)
.distinct
.filterNot(owner => owner == killer.unique || owner == telepadOwner.unique)
.map(p => (WeaponStats(routerKillAssist, size, size, time, 1f), "router-driver", PlayerSource(p, Vector3.Zero))) :+
(WeaponStats(routerTelepadKillAssist, size, size, time, 1f), "telepad-use", telepadOwner)
}
.foreach {
case (equipmentUseContext, equipmentUseEventId, eventOwner) =>
val size = equipmentUseContext.amount
val weaponStat = Support.calculateSupportExperience(equipmentUseEventId, equipmentUseContext)
combineStatsInto(
out,
(
eventOwner.CharId,
ContributionStats(eventOwner, Seq(weaponStat), size, size, size, equipmentUseContext.time)
)
)
}
}
/** /**
* Gather and reward specific in-game equipment use activity.<br> * Gather and reward specific in-game equipment use activity.<br>
* na * na

View file

@ -29,8 +29,13 @@ object Support {
//setup //setup
val historyList = history.toList val historyList = history.toList
val withKills = victim.progress.kills.nonEmpty val withKills = victim.progress.kills.nonEmpty
//TODO Issue #1259 - Use another method to capture time of death than current time ("kill shots" aren't working)
/*
val fullLifespan = (historyList.headOption, historyList.lastOption) match { val fullLifespan = (historyList.headOption, historyList.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time case (Some(spawn), Some(death)) => death.time - spawn.time
*/
val fullLifespan = historyList.headOption match {
case Some(spawn) => System.currentTimeMillis() - spawn.time
case _ => 0L case _ => 0L
} }
val recordOfWornTimes = countTimeWhileExoSuitOrMounted(historyList) val recordOfWornTimes = countTimeWhileExoSuitOrMounted(historyList)
@ -676,12 +681,14 @@ object Support {
val shots = weaponStat.shots val shots = weaponStat.shots
val shotsMax = event.shotsMax val shotsMax = event.shotsMax
val shotsMultiplier = event.shotsMultiplier val shotsMultiplier = event.shotsMultiplier
if (shotsMultiplier > 0f && shots < event.shotsCutoff) { if (shots < event.shotsCutoff) {
val modifiedShotsReward: Float = if (shotsMultiplier > 0f) {
shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat val modifiedShotsReward: Float = shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat
val modifiedAmountReward: Float = val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
event.amountMultiplier * weaponStat.amount.toFloat
event.base.toFloat + modifiedShotsReward + modifiedAmountReward event.base.toFloat + modifiedShotsReward + modifiedAmountReward
} else {
event.base.toFloat
}
} else { } else {
0f 0f
} }

View file

@ -2,11 +2,34 @@
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.newcodecs._ import net.psforever.newcodecs._
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType
import scodec.Codec import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._ import scodec.codecs._
/*
For colors, type '/#n' before text, where `n` is one of the following hexadecimal numbers:
0 white
1 black
2 cyan
3 yellow
4 green
5 light blue
6 brown
7 violet
8 magneta
9 purple
a purple
b yellow green
c blue
d light pink
e light green
f beige
All other options result in white text.
*/
/** /**
* Instructs client to display and/or process a chat message/command when sent server to client. * Instructs client to display and/or process a chat message/command when sent server to client.
* Instructs server to route and/or process a chat message/command when sent client to server. * Instructs server to route and/or process a chat message/command when sent client to server.
@ -35,8 +58,8 @@ final case class ChatMsg(
assert(note.isEmpty, "Note contents found, but message type isnt Note") assert(note.isEmpty, "Note contents found, but message type isnt Note")
type Packet = ChatMsg type Packet = ChatMsg
def opcode = GamePacketOpcode.ChatMsg def opcode: Type = GamePacketOpcode.ChatMsg
def encode = ChatMsg.encode(this) def encode: Attempt[BitVector] = ChatMsg.encode(this)
} }
object ChatMsg extends Marshallable[ChatMsg] { object ChatMsg extends Marshallable[ChatMsg] {

View file

@ -91,4 +91,6 @@ object GenericObjectActionEnum extends Enumeration {
* <b>Target</b>: CaptureTerminal * <b>Target</b>: CaptureTerminal
*/ */
val FlagSpawned = Value(14) val FlagSpawned = Value(14)
val PlayerDeconstructs = Value(6)
val BFRShieldsDown = Value(45)
} }

View file

@ -162,6 +162,10 @@ import scodec.codecs._
* </ul> * </ul>
* `55 - "Someone is attempting to Heal you". Value is 1`<br> * `55 - "Someone is attempting to Heal you". Value is 1`<br>
* `56 - "Someone is attempting to Repair you". Value is 1`<br> * `56 - "Someone is attempting to Repair you". Value is 1`<br>
* `57 - CUD Reveal Friendlies cooldown timer`<br>
* `58 - CUD Reveal Enemies cooldown timer`<br>
* `59 - CUD EMP Blast cooldown timer`<br>
* `60 - CUD Orbital Strike cooldown timer`<br>
* `64 - ????? related to using router telepads` * `64 - ????? related to using router telepads`
* `67 - Enables base shields (from cavern module/lock)`<br> * `67 - Enables base shields (from cavern module/lock)`<br>
* `73 - "You are locked into the Core Beam. Charging your Module now.". Value is 1 to active`<br> * `73 - "You are locked into the Core Beam. Charging your Module now.". Value is 1 to active`<br>

View file

@ -26,15 +26,15 @@ object UplinkRequestType extends IntEnum[UplinkRequestType] {
case object OrbitalStrike extends UplinkRequestType(value = 4) case object OrbitalStrike extends UplinkRequestType(value = 4)
case object Unknown5 extends UplinkRequestType(value = 5) case object Unknown5 extends UplinkRequestType(value = 5) // pull trigger to start orbital strike countdown
case object Function6 extends UplinkRequestType(value = 6) case object Function6 extends UplinkRequestType(value = 6)
case object Function7 extends UplinkRequestType(value = 7) case object Function7 extends UplinkRequestType(value = 7) // sent back by client after reveal enemies response
case object Function8 extends UplinkRequestType(value = 8) case object Function8 extends UplinkRequestType(value = 8)
case object Unknown9 extends UplinkRequestType(value = 9) case object Unknown9 extends UplinkRequestType(value = 9) // recall squad to sanc
case object UnknownA extends UplinkRequestType(value = 10) case object UnknownA extends UplinkRequestType(value = 10)

View file

@ -204,6 +204,7 @@ final case class DetailedCharacterB(
*/ */
final case class DetailedCharacterData(a: DetailedCharacterA, b: DetailedCharacterB)(pad_length: Option[Int]) final case class DetailedCharacterData(a: DetailedCharacterA, b: DetailedCharacterB)(pad_length: Option[Int])
extends ConstructorData { extends ConstructorData {
val padLength: Option[Int] = pad_length
override def bitsize: Long = a.bitsize + b.bitsize override def bitsize: Long = a.bitsize + b.bitsize
} }

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