mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
I FINALLY REFACTORED SESSION ACTOR (#1015)
* chat trying to consume a bang-command when it should not have; hack clear is executed properly again * finally managed to break down SessionActor into something that can be considered 'small files' * the server will start and can be connected to; further testing required * the refactor works correctly; spawn ops moved inot a nested class in zone ops due to sharing; all vaiables should be assigned a scope * removed a layer of pattern matching obfuscating all packet handling methods * moved ownership assignment hopefully corrects issue of player avatar randomly un-owning vehicle * one line changes everything, or nothing, I dunno * if...else to guard booleans during setup * forgot line to avoid MatchError * nesting cases and placing accessors onto a trait's methods
This commit is contained in:
parent
ebfc028f5c
commit
335c4b2099
|
|
@ -9,6 +9,7 @@ import net.psforever.objects.avatar.{Shortcut => AvatarShortcut}
|
||||||
import net.psforever.objects.definition.ImplantDefinition
|
import net.psforever.objects.definition.ImplantDefinition
|
||||||
import net.psforever.packet.game.{CreateShortcutMessage, Shortcut}
|
import net.psforever.packet.game.{CreateShortcutMessage, Shortcut}
|
||||||
import net.psforever.packet.game.objectcreate.DrawnSlot
|
import net.psforever.packet.game.objectcreate.DrawnSlot
|
||||||
|
import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227}
|
||||||
import net.psforever.types.ImplantType
|
import net.psforever.types.ImplantType
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
@ -425,167 +426,11 @@ class ChatActor(
|
||||||
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
|
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
|
||||||
|
|
||||||
/** Messages starting with ! are custom chat commands */
|
/** Messages starting with ! are custom chat commands */
|
||||||
case (messageType, recipient, contents) if contents.startsWith("!") =>
|
case (_, _, contents) if contents.startsWith("!") &&
|
||||||
(messageType, recipient, contents) match {
|
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ;
|
||||||
case (_, _, _contents) if _contents.startsWith("!whitetext ") && session.account.gm =>
|
|
||||||
chatService ! ChatService.Message(
|
|
||||||
session,
|
|
||||||
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
|
||||||
ChatChannel.Default()
|
|
||||||
)
|
|
||||||
|
|
||||||
case (_, _, "!loc") =>
|
|
||||||
val continent = session.zone
|
|
||||||
val player = session.player
|
|
||||||
val loc =
|
|
||||||
s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
|
|
||||||
log.info(loc)
|
|
||||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!list") =>
|
|
||||||
val zone = content.split(" ").lift(1) match {
|
|
||||||
case None =>
|
|
||||||
Some(session.zone)
|
|
||||||
case Some(id) =>
|
|
||||||
Zones.zones.find(_.id == id)
|
|
||||||
}
|
|
||||||
|
|
||||||
zone match {
|
|
||||||
case Some(inZone) =>
|
|
||||||
sessionActor ! SessionActor.SendResponse(
|
|
||||||
ChatMsg(
|
|
||||||
CMT_GMOPEN,
|
|
||||||
message.wideContents,
|
|
||||||
"Server",
|
|
||||||
"\\#8Name (Faction) [ID] at PosX PosY PosZ",
|
|
||||||
message.note
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
(inZone.LivePlayers ++ inZone.Corpses)
|
|
||||||
.filter(_.CharId != session.player.CharId)
|
|
||||||
.sortBy(p => (p.Name, !p.isAlive))
|
|
||||||
.foreach(player => {
|
|
||||||
val color = if (!player.isAlive) "\\#7" else ""
|
|
||||||
sessionActor ! SessionActor.SendResponse(
|
|
||||||
ChatMsg(
|
|
||||||
CMT_GMOPEN,
|
|
||||||
message.wideContents,
|
|
||||||
"Server",
|
|
||||||
s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
|
|
||||||
message.note
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
case None =>
|
|
||||||
sessionActor ! SessionActor.SendResponse(
|
|
||||||
ChatMsg(
|
|
||||||
CMT_GMOPEN,
|
|
||||||
message.wideContents,
|
|
||||||
"Server",
|
|
||||||
"Invalid zone ID",
|
|
||||||
message.note
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!ntu") && gmCommandAllowed =>
|
|
||||||
val buffer = content.toLowerCase.split("\\s+")
|
|
||||||
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
|
|
||||||
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
|
|
||||||
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
|
||||||
case _ => (None, None)
|
|
||||||
}
|
|
||||||
val silos = (facility match {
|
|
||||||
case Some(cur) if cur.toLowerCase().startsWith("curr") =>
|
|
||||||
val position = session.player.Position
|
|
||||||
session.zone.Buildings.values
|
|
||||||
.filter { building =>
|
|
||||||
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
|
|
||||||
Vector3.DistanceSquared(building.Position, position) < soi2
|
|
||||||
}
|
|
||||||
case Some(all) if all.toLowerCase.startsWith("all") =>
|
|
||||||
session.zone.Buildings.values
|
|
||||||
case Some(x) =>
|
|
||||||
session.zone.Buildings.values.find {
|
|
||||||
_.Name.equalsIgnoreCase(x)
|
|
||||||
}.toList
|
|
||||||
case _ =>
|
|
||||||
session.zone.Buildings.values
|
|
||||||
})
|
|
||||||
.flatMap { building => building.Amenities.filter {
|
|
||||||
_.isInstanceOf[ResourceSilo]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
|
|
||||||
val buffer = contents.toLowerCase.split("\\s+")
|
|
||||||
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
|
|
||||||
case Some("-list") | Some("-l") =>
|
|
||||||
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
|
|
||||||
case _ =>
|
|
||||||
CavernRotationService.HurryNextRotation
|
|
||||||
})
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!suicide") =>
|
|
||||||
//this is like CMT_SUICIDE but it ignores checks and forces a suicide state
|
|
||||||
val tplayer = session.player
|
|
||||||
tplayer.Revive
|
|
||||||
tplayer.Actor ! Player.Die()
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!grenade") =>
|
|
||||||
WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
|
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!macro") =>
|
|
||||||
val avatar = session.avatar
|
|
||||||
val args = contents.split(" ").filter(_ != "")
|
|
||||||
(args.lift(1), args.lift(2)) match {
|
|
||||||
case (Some(cmd), other) =>
|
|
||||||
cmd.toLowerCase() match {
|
|
||||||
case "medkit" =>
|
|
||||||
medkitSanityTest(session.player.GUID, avatar.shortcuts)
|
|
||||||
|
|
||||||
case "implants" =>
|
|
||||||
//implant shortcut sanity test
|
|
||||||
implantSanityTest(
|
|
||||||
session.player.GUID,
|
|
||||||
avatar.implants.collect {
|
|
||||||
case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
|
|
||||||
},
|
|
||||||
avatar.shortcuts
|
|
||||||
)
|
|
||||||
|
|
||||||
case name
|
|
||||||
if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
|
|
||||||
avatar.implants.find {
|
|
||||||
case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
|
|
||||||
case None => false
|
|
||||||
} match {
|
|
||||||
case Some(Some(implant)) =>
|
|
||||||
//specific implant shortcut sanity test
|
|
||||||
implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
|
|
||||||
case _ if other.nonEmpty =>
|
|
||||||
//add macro?
|
|
||||||
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
|
|
||||||
case _ => ;
|
|
||||||
}
|
|
||||||
|
|
||||||
case name
|
|
||||||
if name.nonEmpty && other.nonEmpty =>
|
|
||||||
//add macro
|
|
||||||
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
|
|
||||||
|
|
||||||
case _ => ;
|
|
||||||
}
|
|
||||||
case _ => ;
|
|
||||||
// unknown ! commands are ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
|
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
|
||||||
val args = contents.split(" ").filter(_ != "")
|
val args = contents.split(" ").filter(_ != "")
|
||||||
|
|
||||||
val (faction, factionPos): (PlanetSideEmpire.Value, Option[Int]) = args.zipWithIndex
|
val (faction, factionPos): (PlanetSideEmpire.Value, Option[Int]) = args.zipWithIndex
|
||||||
.map { case (factionName, pos) => (factionName.toLowerCase, pos) }
|
.map { case (factionName, pos) => (factionName.toLowerCase, pos) }
|
||||||
.flatMap {
|
.flatMap {
|
||||||
|
|
@ -601,7 +446,6 @@ class ChatActor(
|
||||||
case Some((isFaction, pos)) => (isFaction, Some(pos))
|
case Some((isFaction, pos)) => (isFaction, Some(pos))
|
||||||
case None => (session.player.Faction, None)
|
case None => (session.player.Faction, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (buildingsOption, buildingPos): (Option[Seq[Building]], Option[Int]) = args.zipWithIndex.flatMap {
|
val (buildingsOption, buildingPos): (Option[Seq[Building]], Option[Int]) = args.zipWithIndex.flatMap {
|
||||||
case (_, pos) if factionPos.isDefined && factionPos.get == pos => None
|
case (_, pos) if factionPos.isDefined && factionPos.get == pos => None
|
||||||
case ("all", pos) =>
|
case ("all", pos) =>
|
||||||
|
|
@ -635,7 +479,6 @@ class ChatActor(
|
||||||
case Some((buildings, pos)) => (buildings, pos)
|
case Some((buildings, pos)) => (buildings, pos)
|
||||||
case None => (None, None)
|
case None => (None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (timerOption, timerPos): (Option[Int], Option[Int]) = args.zipWithIndex.flatMap {
|
val (timerOption, timerPos): (Option[Int], Option[Int]) = args.zipWithIndex.flatMap {
|
||||||
case (_, pos)
|
case (_, pos)
|
||||||
if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos =>
|
if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos =>
|
||||||
|
|
@ -1333,4 +1176,190 @@ class ChatActor(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def customCommandMessages(
|
||||||
|
message: ChatMsg,
|
||||||
|
session: Session,
|
||||||
|
chatService: ActorRef[ChatService.Command],
|
||||||
|
cluster: ActorRef[InterstellarClusterService.Command],
|
||||||
|
gmCommandAllowed: Boolean
|
||||||
|
): Boolean = {
|
||||||
|
// val messageType = message.messageType
|
||||||
|
// val recipient = message.recipient
|
||||||
|
val contents = message.contents
|
||||||
|
if (contents.startsWith("!")) {
|
||||||
|
if (contents.startsWith("!whitetext ") && gmCommandAllowed) {
|
||||||
|
chatService ! ChatService.Message(
|
||||||
|
session,
|
||||||
|
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
||||||
|
ChatChannel.Default()
|
||||||
|
)
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!loc ")) {
|
||||||
|
val continent = session.zone
|
||||||
|
val player = session.player
|
||||||
|
val loc =
|
||||||
|
s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
|
||||||
|
log.info(loc)
|
||||||
|
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!list")) {
|
||||||
|
val zone = contents.split(" ").lift(1) match {
|
||||||
|
case None =>
|
||||||
|
Some(session.zone)
|
||||||
|
case Some(id) =>
|
||||||
|
Zones.zones.find(_.id == id)
|
||||||
|
}
|
||||||
|
zone match {
|
||||||
|
case Some(inZone) =>
|
||||||
|
sessionActor ! SessionActor.SendResponse(
|
||||||
|
ChatMsg(
|
||||||
|
CMT_GMOPEN,
|
||||||
|
message.wideContents,
|
||||||
|
"Server",
|
||||||
|
"\\#8Name (Faction) [ID] at PosX PosY PosZ",
|
||||||
|
message.note
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(inZone.LivePlayers ++ inZone.Corpses)
|
||||||
|
.filter(_.CharId != session.player.CharId)
|
||||||
|
.sortBy(p => (p.Name, !p.isAlive))
|
||||||
|
.foreach(player => {
|
||||||
|
val color = if (!player.isAlive) "\\#7" else ""
|
||||||
|
sessionActor ! SessionActor.SendResponse(
|
||||||
|
ChatMsg(
|
||||||
|
CMT_GMOPEN,
|
||||||
|
message.wideContents,
|
||||||
|
"Server",
|
||||||
|
s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
|
||||||
|
message.note
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
case None =>
|
||||||
|
sessionActor ! SessionActor.SendResponse(
|
||||||
|
ChatMsg(
|
||||||
|
CMT_GMOPEN,
|
||||||
|
message.wideContents,
|
||||||
|
"Server",
|
||||||
|
"Invalid zone ID",
|
||||||
|
message.note
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!ntu") && gmCommandAllowed) {
|
||||||
|
val buffer = contents.toLowerCase.split("\\s+")
|
||||||
|
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
|
||||||
|
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
|
||||||
|
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
||||||
|
case _ => (None, None)
|
||||||
|
}
|
||||||
|
val silos = (facility match {
|
||||||
|
case Some(cur) if cur.toLowerCase().startsWith("curr") =>
|
||||||
|
val position = session.player.Position
|
||||||
|
session.zone.Buildings.values
|
||||||
|
.filter { building =>
|
||||||
|
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
|
||||||
|
Vector3.DistanceSquared(building.Position, position) < soi2
|
||||||
|
}
|
||||||
|
case Some(all) if all.toLowerCase.startsWith("all") =>
|
||||||
|
session.zone.Buildings.values
|
||||||
|
case Some(x) =>
|
||||||
|
session.zone.Buildings.values.find {
|
||||||
|
_.Name.equalsIgnoreCase(x)
|
||||||
|
}.toList
|
||||||
|
case _ =>
|
||||||
|
session.zone.Buildings.values
|
||||||
|
})
|
||||||
|
.flatMap { building =>
|
||||||
|
building.Amenities.filter {
|
||||||
|
_.isInstanceOf[ResourceSilo]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!zonerotate") && gmCommandAllowed) {
|
||||||
|
val buffer = contents.toLowerCase.split("\\s+")
|
||||||
|
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
|
||||||
|
case Some("-list") | Some("-l") =>
|
||||||
|
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
|
||||||
|
case _ =>
|
||||||
|
CavernRotationService.HurryNextRotation
|
||||||
|
})
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!suicide")) {
|
||||||
|
//this is like CMT_SUICIDE but it ignores checks and forces a suicide state
|
||||||
|
val tplayer = session.player
|
||||||
|
tplayer.Revive
|
||||||
|
tplayer.Actor ! Player.Die()
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!grenade")) {
|
||||||
|
WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
|
||||||
|
true
|
||||||
|
|
||||||
|
} else if (contents.startsWith("!macro")) {
|
||||||
|
val avatar = session.avatar
|
||||||
|
val args = contents.split(" ").filter(_ != "")
|
||||||
|
(args.lift(1), args.lift(2)) match {
|
||||||
|
case (Some(cmd), other) =>
|
||||||
|
cmd.toLowerCase() match {
|
||||||
|
case "medkit" =>
|
||||||
|
medkitSanityTest(session.player.GUID, avatar.shortcuts)
|
||||||
|
true
|
||||||
|
|
||||||
|
case "implants" =>
|
||||||
|
//implant shortcut sanity test
|
||||||
|
implantSanityTest(
|
||||||
|
session.player.GUID,
|
||||||
|
avatar.implants.collect {
|
||||||
|
case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
|
||||||
|
},
|
||||||
|
avatar.shortcuts
|
||||||
|
)
|
||||||
|
true
|
||||||
|
|
||||||
|
case name
|
||||||
|
if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
|
||||||
|
avatar.implants.find {
|
||||||
|
case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
|
||||||
|
case None => false
|
||||||
|
} match {
|
||||||
|
case Some(Some(implant)) =>
|
||||||
|
//specific implant shortcut sanity test
|
||||||
|
implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
|
||||||
|
true
|
||||||
|
case _ if other.nonEmpty =>
|
||||||
|
//add macro?
|
||||||
|
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
|
||||||
|
true
|
||||||
|
case _ =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
case name
|
||||||
|
if name.nonEmpty && other.nonEmpty =>
|
||||||
|
//add macro
|
||||||
|
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
|
||||||
|
true
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false // unknown ! commands are ignored
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false // unknown ! commands are ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, ActorRef}
|
||||||
|
import net.psforever.objects.avatar.Avatar
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.objects.{Account, Player, Session}
|
||||||
|
import net.psforever.packet.PlanetSideGamePacket
|
||||||
|
import org.log4s.Logger
|
||||||
|
|
||||||
|
trait CommonSessionInterfacingFunctionality {
|
||||||
|
/**
|
||||||
|
* Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class
|
||||||
|
* for which this support class was initialized.
|
||||||
|
* Allows for proper use for `ActorRef.tell` or an actor's `!` in the support class,
|
||||||
|
* one where the result is always directed back to the same `SessionActor` instance.
|
||||||
|
* If there is a different packet "sender" that has to be respected by a given method,
|
||||||
|
* pass that `ActorRef` into the method as a parameter.
|
||||||
|
* @see `ActorRef.!(Any)(ActorRef)`
|
||||||
|
* @see `ActorRef.tell(Any)(ActorRef)`
|
||||||
|
*/
|
||||||
|
protected implicit val sender: ActorRef = context.self
|
||||||
|
|
||||||
|
protected def context: ActorContext
|
||||||
|
|
||||||
|
protected def sessionData: SessionData
|
||||||
|
|
||||||
|
protected def session: Session = sessionData.session
|
||||||
|
|
||||||
|
protected def session_=(newsession: Session): Unit = sessionData.session_=(newsession)
|
||||||
|
|
||||||
|
protected def account: Account = sessionData.account
|
||||||
|
|
||||||
|
protected def continent: Zone = sessionData.continent
|
||||||
|
|
||||||
|
protected def player: Player = sessionData.player
|
||||||
|
|
||||||
|
protected def avatar: Avatar = sessionData.avatar
|
||||||
|
|
||||||
|
protected def log: Logger = sessionData.log
|
||||||
|
|
||||||
|
protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionData.sendResponse(pkt)
|
||||||
|
|
||||||
|
protected[session] def stop(): Unit = { /* to override */ }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,545 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
//
|
||||||
|
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||||
|
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
|
||||||
|
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||||
|
import net.psforever.objects.inventory.InventoryItem
|
||||||
|
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||||
|
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
|
||||||
|
import net.psforever.objects.vital.etc.ExplodingEntityReason
|
||||||
|
import net.psforever.objects.zones.Zoning
|
||||||
|
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
|
||||||
|
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||||
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
|
||||||
|
import net.psforever.services.{InterstellarClusterService => ICS}
|
||||||
|
import net.psforever.types._
|
||||||
|
import net.psforever.util.Config
|
||||||
|
import net.psforever.zones.Zones
|
||||||
|
|
||||||
|
class SessionAvatarHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
chatActor: typed.ActorRef[ChatActor.Command],
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
*
|
||||||
|
* @param toChannel na
|
||||||
|
* @param guid na
|
||||||
|
* @param reply na
|
||||||
|
*/
|
||||||
|
def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
|
||||||
|
val tplayer_guid =
|
||||||
|
if (player != null && player.HasGUID) player.GUID
|
||||||
|
else PlanetSideGUID(0)
|
||||||
|
reply match {
|
||||||
|
case AvatarResponse.TeardownConnection() =>
|
||||||
|
log.trace(s"ending ${player.Name}'s old session by event system request (relog)")
|
||||||
|
context.stop(context.self)
|
||||||
|
|
||||||
|
case AvatarResponse.SendResponse(msg) =>
|
||||||
|
sendResponse(msg)
|
||||||
|
|
||||||
|
case AvatarResponse.SendResponseTargeted(target_guid, msg) =>
|
||||||
|
if (tplayer_guid == target_guid) {
|
||||||
|
sendResponse(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.Revive(target_guid) =>
|
||||||
|
if (tplayer_guid == target_guid) {
|
||||||
|
log.info(s"No time for rest, ${player.Name}. Back on your feet!")
|
||||||
|
sessionData.zoning.spawn.reviveTimer.cancel()
|
||||||
|
sessionData.zoning.spawn.deadState = DeadState.Alive
|
||||||
|
player.Revive
|
||||||
|
val health = player.Health
|
||||||
|
sendResponse(PlanetsideAttributeMessage(target_guid, 0, health))
|
||||||
|
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, player.Position, player.Faction, unk5=true))
|
||||||
|
continent.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
AvatarAction.PlanetsideAttributeToAll(target_guid, 0, health)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ArmorChanged(suit, subtype) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ArmorChangedMessage(guid, suit, subtype))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateMessage(
|
||||||
|
ammo_id,
|
||||||
|
ammo_guid,
|
||||||
|
ObjectCreateMessageParent(weapon_guid, weapon_slot),
|
||||||
|
ammo_data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sendResponse(ChangeAmmoMessage(weapon_guid, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ChangeFireMode(item_guid, mode) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ChangeFireModeMessage(item_guid, mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ChangeFireState_Start(weapon_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ChangeFireStateMessage_Start(weapon_guid))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ChangeFireState_Stop(weapon_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ChangeFireStateMessage_Stop(weapon_guid))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ConcealPlayer() =>
|
||||||
|
sendResponse(GenericObjectActionMessage(guid, 9))
|
||||||
|
|
||||||
|
case AvatarResponse.EnvironmentalDamage(_, _, _) =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
||||||
|
//TODO damage marker?
|
||||||
|
|
||||||
|
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(sessionData.DestroyDisplayMessage(killer, victim, method, unk))
|
||||||
|
// TODO Temporary thing that should go somewhere else and use proper xp values
|
||||||
|
if (killer.CharId == avatar.id && killer.Faction != victim.Faction) {
|
||||||
|
avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong)
|
||||||
|
avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.DropItem(pkt) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.EquipmentInHand(pkt) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.GenericObjectAction(object_guid, action_code) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(GenericObjectActionMessage(object_guid, action_code))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.HitHint(source_guid) =>
|
||||||
|
if (player.isAlive) {
|
||||||
|
sendResponse(HitHint(source_guid, guid))
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.DropSpecialItem() =>
|
||||||
|
sessionData.DropSpecialSlotItem()
|
||||||
|
|
||||||
|
case AvatarResponse.Killed(mount) =>
|
||||||
|
val cause = (player.LastDamage match {
|
||||||
|
case Some(reason) => (Some(reason), reason.adversarial)
|
||||||
|
case None => (None, None)
|
||||||
|
}) match {
|
||||||
|
case (_, Some(adversarial)) => adversarial.attacker.Name
|
||||||
|
case (Some(reason), None) => s"a ${reason.interaction.cause.getClass.getSimpleName}"
|
||||||
|
case _ => s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)"
|
||||||
|
}
|
||||||
|
log.info(s"${player.Name} has died, killed by $cause")
|
||||||
|
val respawnTimer = 300.seconds
|
||||||
|
//drop free hand item
|
||||||
|
player.FreeHand.Equipment match {
|
||||||
|
case Some(item) =>
|
||||||
|
DropEquipmentFromInventory(player)(item)
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
sessionData.DropSpecialSlotItem()
|
||||||
|
sessionData.ToggleMaxSpecialState(enable = false)
|
||||||
|
if (player.LastDamage match {
|
||||||
|
case Some(damage) => damage.interaction.cause match {
|
||||||
|
case cause: ExplodingEntityReason => cause.entity.isInstanceOf[VehicleSpawnPad]
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
case None => false
|
||||||
|
}) {
|
||||||
|
//also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
|
||||||
|
sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@SVCP_Killed_OnPadOnCreate", None))
|
||||||
|
}
|
||||||
|
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||||
|
sessionData.zoning.zoningStatus = Zoning.Status.None
|
||||||
|
sessionData.zoning.spawn.deadState = DeadState.Dead
|
||||||
|
continent.GUID(mount) match {
|
||||||
|
case Some(obj: Vehicle) =>
|
||||||
|
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
|
||||||
|
sessionData.vehicles.serverVehicleControlVelocity = None
|
||||||
|
sessionData.UnaccessContainer(obj)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
sessionData.PlayerActionsToCancel()
|
||||||
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
|
||||||
|
if (sessionData.shooting.shotsWhileDead > 0) {
|
||||||
|
log.warn(
|
||||||
|
s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionData.shooting.shotsWhileDead} rounds while character was dead on server"
|
||||||
|
)
|
||||||
|
sessionData.shooting.shotsWhileDead = 0
|
||||||
|
}
|
||||||
|
sessionData.zoning.spawn.reviveTimer.cancel()
|
||||||
|
if (player.death_by == 0) {
|
||||||
|
sessionData.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) {
|
||||||
|
sessionData.cluster ! ICS.GetRandomSpawnPoint(
|
||||||
|
Zones.sanctuaryZoneNumber(player.Faction),
|
||||||
|
player.Faction,
|
||||||
|
Seq(SpawnGroup.Sanctuary),
|
||||||
|
context.self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sessionData.zoning.spawn.HandleReleaseAvatar(player, continent)
|
||||||
|
}
|
||||||
|
AvatarActor.savePlayerLocation(player)
|
||||||
|
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
|
||||||
|
|
||||||
|
case AvatarResponse.LoadPlayer(pkt) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.LoadProjectile(pkt) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ObjectDelete(item_guid, unk) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ObjectDeleteMessage(item_guid, unk))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ObjectHeld(slot, previousSlot) =>
|
||||||
|
if (tplayer_guid == guid) {
|
||||||
|
if (slot > -1) {
|
||||||
|
sendResponse(ObjectHeldMessage(guid, slot, unk1 = true))
|
||||||
|
//Stop using proximity terminals if player unholsters a weapon
|
||||||
|
if (player.VisibleSlots.contains(slot)) {
|
||||||
|
continent.GUID(sessionData.terminals.usingMedicalTerminal) match {
|
||||||
|
case Some(term: Terminal with ProximityUnit) =>
|
||||||
|
sessionData.terminals.StopUsingProximityUnit(term)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendResponse(ObjectHeldMessage(guid, previousSlot, unk1 = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.OxygenState(player, vehicle) =>
|
||||||
|
sendResponse(
|
||||||
|
OxygenStateMessage(
|
||||||
|
DrowningTarget(player.guid, player.progress, player.state),
|
||||||
|
vehicle match {
|
||||||
|
case Some(vinfo) => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state))
|
||||||
|
case None => None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.PlanetsideAttributeToAll(attribute_type, attribute_value) =>
|
||||||
|
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
|
||||||
|
|
||||||
|
case AvatarResponse.PlanetsideAttributeSelf(attribute_type, attribute_value) =>
|
||||||
|
if (tplayer_guid == guid) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.PlayerState(
|
||||||
|
pos,
|
||||||
|
vel,
|
||||||
|
yaw,
|
||||||
|
pitch,
|
||||||
|
yaw_upper,
|
||||||
|
_,
|
||||||
|
is_crouching,
|
||||||
|
is_jumping,
|
||||||
|
jump_thrust,
|
||||||
|
is_cloaking,
|
||||||
|
spectating,
|
||||||
|
_
|
||||||
|
) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val (location, time, distanceSq): (Vector3, Long, Float) = if (spectating) {
|
||||||
|
val r = new scala.util.Random
|
||||||
|
val r1 = 2 + r.nextInt(30).toFloat
|
||||||
|
val r2 = 2 + r.nextInt(4000).toFloat
|
||||||
|
(Vector3(r2, r2, r1), 0L, 0f)
|
||||||
|
} else {
|
||||||
|
val before = player.lastSeenStreamMessage(guid.guid)
|
||||||
|
val dist = Vector3.DistanceSquared(player.Position, pos)
|
||||||
|
(pos, now - before, dist)
|
||||||
|
}
|
||||||
|
if (distanceSq < 302500 || time > 5000) { // Render distance seems to be approx 525m. Reduce update rate at ~550m to be safe
|
||||||
|
sendResponse(
|
||||||
|
PlayerStateMessage(
|
||||||
|
guid,
|
||||||
|
location,
|
||||||
|
vel,
|
||||||
|
yaw,
|
||||||
|
pitch,
|
||||||
|
yaw_upper,
|
||||||
|
timestamp = 0,
|
||||||
|
is_crouching,
|
||||||
|
is_jumping,
|
||||||
|
jump_thrust,
|
||||||
|
is_cloaking
|
||||||
|
)
|
||||||
|
)
|
||||||
|
player.lastSeenStreamMessage(guid.guid) = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ProjectileExplodes(projectile_guid, projectile) =>
|
||||||
|
sendResponse(
|
||||||
|
ProjectileStateMessage(
|
||||||
|
projectile_guid,
|
||||||
|
projectile.Position,
|
||||||
|
Vector3.Zero,
|
||||||
|
projectile.Orientation,
|
||||||
|
0,
|
||||||
|
end=true,
|
||||||
|
PlanetSideGUID(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sendResponse(ObjectDeleteMessage(projectile_guid, 2))
|
||||||
|
|
||||||
|
case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
|
||||||
|
sendResponse(GenericActionMessage(mode))
|
||||||
|
|
||||||
|
case AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.PutDownFDU(target) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(GenericObjectActionMessage(target, 53))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.Release(tplayer) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sessionData.zoning.spawn.DepictPlayerAsCorpse(tplayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.Reload(item_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ReloadMessage(item_guid, 1, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.SetEmpire(object_guid, faction) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(SetEmpireMessage(object_guid, faction))
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.StowEquipment(target, slot, item) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
val definition = item.Definition
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateDetailedMessage(
|
||||||
|
definition.ObjectId,
|
||||||
|
item.GUID,
|
||||||
|
ObjectCreateMessageParent(target, slot),
|
||||||
|
definition.Packet.DetailedConstructorData(item).get
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.WeaponDryFire(weapon_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
continent.GUID(weapon_guid) match {
|
||||||
|
case Some(tool: Tool) =>
|
||||||
|
// check that the magazine is still empty before sending WeaponDryFireMessage
|
||||||
|
// if it has been reloaded since then, other clients not see it firing
|
||||||
|
if (tool.Magazine == 0) {
|
||||||
|
sendResponse(WeaponDryFireMessage(weapon_guid))
|
||||||
|
}
|
||||||
|
case Some(_) =>
|
||||||
|
sendResponse(WeaponDryFireMessage(weapon_guid))
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.TerminalOrderResult(terminal_guid, action, result) =>
|
||||||
|
sendResponse(ItemTransactionResultMessage(terminal_guid, action, result))
|
||||||
|
sessionData.terminals.lastTerminalOrderFulfillment = true
|
||||||
|
if (result &&
|
||||||
|
(action == TransactionType.Buy || action == TransactionType.Loadout)) {
|
||||||
|
AvatarActor.savePlayerData(player)
|
||||||
|
sessionData.renewCharSavedTimer(
|
||||||
|
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||||
|
Config.app.game.savedMsg.interruptedByAction.variable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.ChangeExosuit(
|
||||||
|
target,
|
||||||
|
armor,
|
||||||
|
exosuit,
|
||||||
|
subtype,
|
||||||
|
slot,
|
||||||
|
maxhand,
|
||||||
|
old_holsters,
|
||||||
|
holsters,
|
||||||
|
old_inventory,
|
||||||
|
inventory,
|
||||||
|
drop,
|
||||||
|
delete
|
||||||
|
) =>
|
||||||
|
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(target, 4, armor))
|
||||||
|
if (tplayer_guid == target) {
|
||||||
|
//happening to this player
|
||||||
|
//cleanup
|
||||||
|
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
|
||||||
|
(old_holsters ++ old_inventory ++ delete).foreach {
|
||||||
|
case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, 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 match {
|
||||||
|
case Some(obj) =>
|
||||||
|
val definition = obj.Definition
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateDetailedMessage(
|
||||||
|
definition.ObjectId,
|
||||||
|
obj.GUID,
|
||||||
|
ObjectCreateMessageParent(target, Player.FreeHandSlot),
|
||||||
|
definition.Packet.DetailedConstructorData(obj).get
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
//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)
|
||||||
|
} else {
|
||||||
|
//happening to some other player
|
||||||
|
sendResponse(ObjectHeldMessage(target, slot, unk1=false))
|
||||||
|
//cleanup
|
||||||
|
(old_holsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 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,
|
||||||
|
slot,
|
||||||
|
maxhand,
|
||||||
|
old_holsters,
|
||||||
|
holsters,
|
||||||
|
old_inventory,
|
||||||
|
inventory,
|
||||||
|
drops
|
||||||
|
) =>
|
||||||
|
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(target, 4, armor))
|
||||||
|
if (tplayer_guid == target) {
|
||||||
|
//happening to this player
|
||||||
|
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
|
||||||
|
//cleanup
|
||||||
|
(old_holsters ++ old_inventory).foreach {
|
||||||
|
case (obj, objGuid) =>
|
||||||
|
sendResponse(ObjectDeleteMessage(objGuid, 0))
|
||||||
|
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||||
|
}
|
||||||
|
//redraw
|
||||||
|
if (maxhand) {
|
||||||
|
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
||||||
|
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
||||||
|
slot = 0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
sessionData.ApplyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
|
||||||
|
DropLeftovers(player)(drops)
|
||||||
|
} else {
|
||||||
|
//happening to some other player
|
||||||
|
sendResponse(ObjectHeldMessage(target, slot, unk1=false))
|
||||||
|
//cleanup
|
||||||
|
old_holsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) }
|
||||||
|
//redraw handled by callback
|
||||||
|
}
|
||||||
|
|
||||||
|
case AvatarResponse.UseKit(kguid, kObjId) =>
|
||||||
|
sendResponse(
|
||||||
|
UseItemMessage(
|
||||||
|
tplayer_guid,
|
||||||
|
kguid,
|
||||||
|
tplayer_guid,
|
||||||
|
4294967295L,
|
||||||
|
unk3=false,
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.Zero,
|
||||||
|
126,
|
||||||
|
0, //sequence time?
|
||||||
|
137,
|
||||||
|
kObjId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sendResponse(ObjectDeleteMessage(kguid, 0))
|
||||||
|
|
||||||
|
case AvatarResponse.KitNotUsed(_, "") =>
|
||||||
|
sessionData.kitToBeUsed = None
|
||||||
|
|
||||||
|
case AvatarResponse.KitNotUsed(_, msg) =>
|
||||||
|
sessionData.kitToBeUsed = None
|
||||||
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", msg, None))
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, ActorRef, typed}
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
//
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
import net.psforever.objects.Vehicle
|
||||||
|
import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage}
|
||||||
|
import net.psforever.services.Service
|
||||||
|
import net.psforever.services.galaxy.GalaxyResponse
|
||||||
|
import net.psforever.types.{MemberAction, PlanetSideEmpire}
|
||||||
|
|
||||||
|
class SessionGalaxyHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
galaxyService: ActorRef,
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
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) =>
|
||||||
|
val playerName = player.Name
|
||||||
|
log.debug(s"TransferPassenger: $playerName received the summons to transfer to ${vehicle.Zone.id} ...")
|
||||||
|
(manifest.passengers.find { _.name.equals(playerName) } match {
|
||||||
|
case Some(entry) if vehicle.Seats(entry.mount).occupant.isEmpty =>
|
||||||
|
player.VehicleSeated = None
|
||||||
|
vehicle.Seats(entry.mount).mount(player)
|
||||||
|
player.VehicleSeated = vehicle.GUID
|
||||||
|
Some(vehicle)
|
||||||
|
case Some(entry) if vehicle.Seats(entry.mount).occupant.contains(player) =>
|
||||||
|
Some(vehicle)
|
||||||
|
case Some(entry) =>
|
||||||
|
log.warn(
|
||||||
|
s"TransferPassenger: $playerName tried to mount seat ${entry.mount} during summoning, but it was already occupied, and ${player.Sex.pronounSubject} was rebuked"
|
||||||
|
)
|
||||||
|
None
|
||||||
|
case None =>
|
||||||
|
//log.warn(s"TransferPassenger: $playerName is missing from the manifest of a summoning ${vehicle.Definition.Name} from ${vehicle.Zone.id}")
|
||||||
|
None
|
||||||
|
}).orElse {
|
||||||
|
manifest.cargo.find { _.name.equals(playerName) } match {
|
||||||
|
case Some(entry) =>
|
||||||
|
vehicle.CargoHolds(entry.mount).occupant match {
|
||||||
|
case out @ Some(cargo) if cargo.Seats(0).occupants.exists(_.Name.equals(playerName)) =>
|
||||||
|
out
|
||||||
|
case _ =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} match {
|
||||||
|
case Some(v: Vehicle) =>
|
||||||
|
galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above)
|
||||||
|
sessionData.zoning.spawn.deadState = DeadState.Release
|
||||||
|
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
|
||||||
|
sessionData.zoning.interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system
|
||||||
|
sessionData.zoning.spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None)
|
||||||
|
case _ =>
|
||||||
|
sessionData.zoning.interstellarFerry match {
|
||||||
|
case None =>
|
||||||
|
galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
|
||||||
|
sessionData.zoning.interstellarFerryTopLevelGUID = None
|
||||||
|
case Some(_) => ;
|
||||||
|
//wait patiently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.ActorContext
|
||||||
|
import net.psforever.objects.ce.Deployable
|
||||||
|
import net.psforever.objects.vehicles.MountableWeapons
|
||||||
|
import net.psforever.objects._
|
||||||
|
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
||||||
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.services.local.LocalResponse
|
||||||
|
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
|
||||||
|
|
||||||
|
class SessionLocalHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param toChannel na
|
||||||
|
* @param guid na
|
||||||
|
* @param reply na
|
||||||
|
*/
|
||||||
|
def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
|
||||||
|
val tplayer_guid = if (player.HasGUID) { player.GUID }
|
||||||
|
else { PlanetSideGUID(0) }
|
||||||
|
reply match {
|
||||||
|
case LocalResponse.DeployableMapIcon(behavior, deployInfo) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.DeployableUIFor(item) =>
|
||||||
|
sessionData.UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
|
||||||
|
|
||||||
|
case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
|
||||||
|
sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(dguid, 29, 1))
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
|
||||||
|
case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
|
||||||
|
sendResponse(GenericObjectActionMessage(dguid, 19))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(dguid, 29, 1))
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
|
||||||
|
case LocalResponse.Detonate(_, obj) =>
|
||||||
|
log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
|
||||||
|
|
||||||
|
case LocalResponse.DoorOpens(door_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(GenericObjectStateMsg(door_guid, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.DoorCloses(door_guid) => //door closes for everyone
|
||||||
|
sendResponse(GenericObjectStateMsg(door_guid, 17))
|
||||||
|
|
||||||
|
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
|
||||||
|
if (obj.Destroyed) {
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
} else {
|
||||||
|
obj.Destroyed = true
|
||||||
|
DeconstructDeployable(
|
||||||
|
obj,
|
||||||
|
dguid,
|
||||||
|
pos,
|
||||||
|
obj.Orientation,
|
||||||
|
if (obj.MountPoints.isEmpty) 2 else 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
|
||||||
|
if (obj.Destroyed || obj.Jammed || obj.Health == 0) {
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
} else {
|
||||||
|
obj.Destroyed = true
|
||||||
|
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
|
||||||
|
//if active, deactivate
|
||||||
|
if (obj.Active) {
|
||||||
|
obj.Active = false
|
||||||
|
sendResponse(GenericObjectActionMessage(dguid, 29))
|
||||||
|
sendResponse(GenericObjectActionMessage(dguid, 30))
|
||||||
|
}
|
||||||
|
//standard deployable elimination behavior
|
||||||
|
if (obj.Destroyed) {
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
} else {
|
||||||
|
obj.Destroyed = true
|
||||||
|
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType = 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
|
||||||
|
if (obj.Destroyed) {
|
||||||
|
sendResponse(ObjectDeleteMessage(dguid, 0))
|
||||||
|
} else {
|
||||||
|
obj.Destroyed = true
|
||||||
|
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2) =>
|
||||||
|
sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))
|
||||||
|
|
||||||
|
case LocalResponse.HackObject(target_guid, unk1, unk2) =>
|
||||||
|
HackObject(target_guid, unk1, unk2)
|
||||||
|
|
||||||
|
case LocalResponse.SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value) =>
|
||||||
|
SendPlanetsideAttributeMessage(target_guid, attribute_number, attribute_value)
|
||||||
|
|
||||||
|
case LocalResponse.SendGenericObjectActionMessage(target_guid, action_number) =>
|
||||||
|
sendResponse(GenericObjectActionMessage(target_guid, action_number))
|
||||||
|
|
||||||
|
case LocalResponse.SendGenericActionMessage(action_number) =>
|
||||||
|
sendResponse(GenericActionMessage(action_number))
|
||||||
|
|
||||||
|
case LocalResponse.SendChatMsg(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, 0.8000001f))
|
||||||
|
|
||||||
|
case LocalResponse.LluDespawned(llu) =>
|
||||||
|
sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, llu.Position, unk = 20, 0.8000001f))
|
||||||
|
sendResponse(ObjectDeleteMessage(llu.GUID, 0))
|
||||||
|
// If the player was holding the LLU, remove it from their tracked special item slot
|
||||||
|
sessionData.specialItemSlotGuid match {
|
||||||
|
case Some(guid) =>
|
||||||
|
if (guid == llu.GUID) {
|
||||||
|
sessionData.specialItemSlotGuid = None
|
||||||
|
player.Carrying = None
|
||||||
|
}
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.ObjectDelete(object_guid, unk) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ObjectDeleteMessage(object_guid, unk))
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
|
||||||
|
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, unk=true))
|
||||||
|
|
||||||
|
case LocalResponse.ProximityTerminalEffect(object_guid, false) =>
|
||||||
|
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, unk=false))
|
||||||
|
sessionData.terminals.ForgetAllProximityTerminals(object_guid)
|
||||||
|
|
||||||
|
case LocalResponse.RouterTelepadMessage(msg) =>
|
||||||
|
sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, "", msg, None))
|
||||||
|
|
||||||
|
case LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid) =>
|
||||||
|
sessionData.UseRouterTelepadEffect(passenger_guid, src_guid, dest_guid)
|
||||||
|
|
||||||
|
case LocalResponse.SendResponse(msg) =>
|
||||||
|
sendResponse(msg)
|
||||||
|
|
||||||
|
case LocalResponse.SetEmpire(object_guid, empire) =>
|
||||||
|
sendResponse(SetEmpireMessage(object_guid, empire))
|
||||||
|
|
||||||
|
case LocalResponse.ShuttleEvent(ev) =>
|
||||||
|
val msg = OrbitalShuttleTimeMsg(
|
||||||
|
ev.u1,
|
||||||
|
ev.u2,
|
||||||
|
ev.t1,
|
||||||
|
ev.t2,
|
||||||
|
ev.t3,
|
||||||
|
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, 0, pos, orient, None, Some(state), 0, 0, 15, is_decelerating=false, is_cloaked=false))
|
||||||
|
|
||||||
|
case LocalResponse.ToggleTeleportSystem(router, system_plan) =>
|
||||||
|
sessionData.ToggleTeleportSystem(router, system_plan)
|
||||||
|
|
||||||
|
case LocalResponse.TriggerEffect(target_guid, effect, effectInfo, triggerLocation) =>
|
||||||
|
sendResponse(TriggerEffectMessage(target_guid, effect, effectInfo, triggerLocation))
|
||||||
|
|
||||||
|
case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
|
||||||
|
sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
|
||||||
|
|
||||||
|
case LocalResponse.UpdateForceDomeStatus(building_guid, activated) =>
|
||||||
|
if (activated) {
|
||||||
|
sendResponse(GenericObjectActionMessage(building_guid, 11))
|
||||||
|
} else {
|
||||||
|
sendResponse(GenericObjectActionMessage(building_guid, 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) =>
|
||||||
|
if (tplayer_guid == guid) {
|
||||||
|
continent.GUID(vehicle_guid) match {
|
||||||
|
case Some(vehicle: MountableWeapons) =>
|
||||||
|
vehicle.PassengerInSeat(player) match {
|
||||||
|
case Some(seat_num: Int) =>
|
||||||
|
vehicle.WeaponControlledFromSeat(seat_num) foreach {
|
||||||
|
case weapon: Tool if weapon.GUID == weapon_guid =>
|
||||||
|
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param target_guid na
|
||||||
|
* @param unk1 na
|
||||||
|
* @param unk2 na
|
||||||
|
*/
|
||||||
|
def HackObject(target_guid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
|
||||||
|
sendResponse(HackMessage(0, target_guid, PlanetSideGUID(0), 100, unk1, HackState.Hacked, unk2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a PlanetsideAttributeMessage packet to the client
|
||||||
|
* @param target_guid The target of the attribute
|
||||||
|
* @param attribute_number The attribute number
|
||||||
|
* @param attribute_value The attribute value
|
||||||
|
*/
|
||||||
|
def SendPlanetsideAttributeMessage(
|
||||||
|
target_guid: PlanetSideGUID,
|
||||||
|
attribute_number: PlanetsideAttributeEnum,
|
||||||
|
attribute_value: Long
|
||||||
|
): Unit = {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(target_guid, attribute_number, attribute_value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
//
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
import net.psforever.actors.zone.ZoneActor
|
||||||
|
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
|
||||||
|
import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
|
||||||
|
import net.psforever.objects.serverobject.mount.Mountable
|
||||||
|
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.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleMsg, GenericObjectActionMessage, ObjectAttachMessage, 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, PlanetSideGUID}
|
||||||
|
|
||||||
|
class SessionMountHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
*
|
||||||
|
* @param tplayer na
|
||||||
|
* @param reply na
|
||||||
|
*/
|
||||||
|
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
|
||||||
|
reply match {
|
||||||
|
case Mountable.CanMount(obj: ImplantTerminalMech, seat_number, _) =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||||
|
log.info(s"${player.Name} mounts an implant terminal")
|
||||||
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
|
MountingAction(tplayer, obj, seat_number)
|
||||||
|
sessionData.keepAliveFunc = sessionData.KeepAlivePersistence
|
||||||
|
|
||||||
|
case Mountable.CanMount(obj: Vehicle, seat_number, _) if obj.Definition == GlobalDefinitions.orbital_shuttle =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||||
|
log.info(s"${player.Name} mounts the orbital shuttle")
|
||||||
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
|
MountingAction(tplayer, obj, seat_number)
|
||||||
|
sessionData.keepAliveFunc = sessionData.KeepAlivePersistence
|
||||||
|
|
||||||
|
case Mountable.CanMount(obj: Vehicle, seat_number, _) =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||||
|
log.info(s"${player.Name} mounts the ${obj.Definition.Name} in ${
|
||||||
|
obj.SeatPermissionGroup(seat_number) match {
|
||||||
|
case Some(AccessPermissionGroup.Driver) => "the driver seat"
|
||||||
|
case Some(seatType) => s"a $seatType seat (#$seat_number)"
|
||||||
|
case None => "a seat"
|
||||||
|
}
|
||||||
|
}")
|
||||||
|
val obj_guid: PlanetSideGUID = obj.GUID
|
||||||
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||||
|
if (obj.Definition == GlobalDefinitions.ant) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 45, obj.NtuCapacitorScaled))
|
||||||
|
}
|
||||||
|
if (obj.Definition.MaxCapacitor > 0) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 113, obj.Capacitor))
|
||||||
|
}
|
||||||
|
if (seat_number == 0) {
|
||||||
|
if (obj.Definition == GlobalDefinitions.quadstealth) {
|
||||||
|
//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, 11))
|
||||||
|
} else if (obj.WeaponControlledFromSeat(seat_number).isEmpty) {
|
||||||
|
sessionData.keepAliveFunc = sessionData.KeepAlivePersistence
|
||||||
|
}
|
||||||
|
sessionData.AccessContainer(obj)
|
||||||
|
sessionData.UpdateWeaponAtSeatPosition(obj, seat_number)
|
||||||
|
MountingAction(tplayer, obj, seat_number)
|
||||||
|
|
||||||
|
case Mountable.CanMount(obj: FacilityTurret, seat_number, _) =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||||
|
if (!obj.isUpgrading) {
|
||||||
|
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||||
|
if (obj.Definition == GlobalDefinitions.vanu_sentry_turret) {
|
||||||
|
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
|
||||||
|
}
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
|
||||||
|
sessionData.UpdateWeaponAtSeatPosition(obj, seat_number)
|
||||||
|
MountingAction(tplayer, obj, seat_number)
|
||||||
|
} else {
|
||||||
|
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 WeaponTurret, seat_number, _) =>
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||||
|
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
||||||
|
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
|
||||||
|
sessionData.UpdateWeaponAtSeatPosition(obj, seat_number)
|
||||||
|
MountingAction(tplayer, obj, seat_number)
|
||||||
|
|
||||||
|
case Mountable.CanMount(obj: Mountable, _, _) =>
|
||||||
|
log.warn(s"MountVehicleMsg: $obj is some mountable object and nothing will happen for ${player.Name}")
|
||||||
|
|
||||||
|
case Mountable.CanDismount(obj: ImplantTerminalMech, seat_num, _) =>
|
||||||
|
log.info(s"${tplayer.Name} dismounts the implant terminal")
|
||||||
|
DismountAction(tplayer, obj, seat_num)
|
||||||
|
|
||||||
|
case Mountable.CanDismount(obj: Vehicle, seat_num, mount_point)
|
||||||
|
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
|
||||||
|
val pguid = player.GUID
|
||||||
|
if (obj.MountedIn.nonEmpty) {
|
||||||
|
//dismount to hart lobby
|
||||||
|
log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
|
||||||
|
val sguid = obj.GUID
|
||||||
|
val (pos, zang) = Vehicles.dismountShuttle(obj, mount_point)
|
||||||
|
tplayer.Position = pos
|
||||||
|
sendResponse(DelayedPathMountMsg(pguid, sguid, 60, u2=true))
|
||||||
|
continent.LocalEvents ! LocalServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, 0, 0, zang))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.info(s"${player.Name} is prepped for dropping")
|
||||||
|
//get ready for orbital drop
|
||||||
|
DismountAction(tplayer, obj, seat_num)
|
||||||
|
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
|
||||||
|
continent.VehicleEvents ! 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
|
||||||
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
player.Name,
|
||||||
|
VehicleAction.SendResponse(
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
PlayerStateShiftMessage(ShiftState(0, obj.Position, obj.Orientation.z, None)) //cower in the shuttle bay
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, 9)) //conceal the player
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||||
|
|
||||||
|
case Mountable.CanDismount(obj: Vehicle, seat_num, _) if obj.Definition == GlobalDefinitions.droppod =>
|
||||||
|
log.info(s"${tplayer.Name} has landed on ${continent.id}")
|
||||||
|
sessionData.UnaccessContainer(obj)
|
||||||
|
DismountAction(tplayer, obj, seat_num)
|
||||||
|
obj.Actor ! Vehicle.Deconstruct()
|
||||||
|
|
||||||
|
case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
|
||||||
|
val player_guid: PlanetSideGUID = tplayer.GUID
|
||||||
|
if (player_guid == player.GUID) {
|
||||||
|
//disembarking self
|
||||||
|
log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
|
||||||
|
obj.SeatPermissionGroup(seat_num) match {
|
||||||
|
case Some(AccessPermissionGroup.Driver) => "driver seat"
|
||||||
|
case Some(seatType) => s"$seatType seat (#$seat_num)"
|
||||||
|
case None => "seat"
|
||||||
|
}
|
||||||
|
}")
|
||||||
|
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
|
||||||
|
sessionData.UnaccessContainer(obj)
|
||||||
|
DismountAction(tplayer, obj, seat_num)
|
||||||
|
} else {
|
||||||
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
VehicleAction.KickPassenger(player_guid, seat_num, unk2=true, obj.GUID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seat_num, _) =>
|
||||||
|
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
|
||||||
|
DismountAction(tplayer, obj, seat_num)
|
||||||
|
|
||||||
|
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, mount_point) =>
|
||||||
|
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed")
|
||||||
|
obj.GetSeatFromMountPoint(mount_point) match {
|
||||||
|
case Some(seatNum) if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
|
||||||
|
sendResponse(
|
||||||
|
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "You are not the driver of this vehicle.", None)
|
||||||
|
)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
|
||||||
|
case Mountable.CanNotMount(obj: Mountable, mount_point) =>
|
||||||
|
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed")
|
||||||
|
|
||||||
|
case Mountable.CanNotDismount(obj, seat_num) =>
|
||||||
|
log.warn(
|
||||||
|
s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seat_num, but was not allowed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Mountable, seatNum: Int): Unit = {
|
||||||
|
val player_guid: PlanetSideGUID = tplayer.GUID
|
||||||
|
val obj_guid: PlanetSideGUID = obj.GUID
|
||||||
|
sessionData.PlayerActionsToCancel()
|
||||||
|
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||||
|
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3 seconds)
|
||||||
|
sendResponse(ObjectAttachMessage(obj_guid, player_guid, seatNum))
|
||||||
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
VehicleAction.MountVehicle(player_guid, obj_guid, 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 DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = {
|
||||||
|
val player_guid: PlanetSideGUID = tplayer.GUID
|
||||||
|
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||||
|
val bailType = if (tplayer.BailProtection) {
|
||||||
|
BailType.Bailed
|
||||||
|
} else {
|
||||||
|
BailType.Normal
|
||||||
|
}
|
||||||
|
sendResponse(DismountVehicleMsg(player_guid, bailType, wasKickedByDriver = false))
|
||||||
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
VehicleAction.DismountVehicle(player_guid, bailType, unk2=false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,670 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, ActorRef, typed}
|
||||||
|
import scala.collection.mutable
|
||||||
|
//
|
||||||
|
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||||
|
import net.psforever.objects.avatar.Avatar
|
||||||
|
import net.psforever.objects.teamwork.Squad
|
||||||
|
import net.psforever.objects.{Default, LivePlayerList, Player}
|
||||||
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
import net.psforever.services.chat.ChatService
|
||||||
|
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
|
||||||
|
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadListDecoration, SquadResponseType, Vector3, WaypointSubtype}
|
||||||
|
|
||||||
|
object SessionSquadHandlers {
|
||||||
|
protected final case class SquadUIElement(
|
||||||
|
name: String,
|
||||||
|
outfit: Long,
|
||||||
|
index: Int,
|
||||||
|
zone: Int,
|
||||||
|
health: Int,
|
||||||
|
armor: Int,
|
||||||
|
position: Vector3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionSquadHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
chatActor: typed.ActorRef[ChatActor.Command],
|
||||||
|
squadService: ActorRef,
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
import SessionSquadHandlers._
|
||||||
|
|
||||||
|
private var waypointCooldown: Long = 0L
|
||||||
|
val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]()
|
||||||
|
var squad_supplement_id: Int = 0
|
||||||
|
/**
|
||||||
|
* When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
|
||||||
|
* This `WorldSessionActor`-local variable is then used to indicate the ongoing state of the LFS UI component,
|
||||||
|
* now called "Looking for Squad Member."
|
||||||
|
* Only the squad leader may toggle the LFSM marquee.
|
||||||
|
* Upon leaving or disbanding a squad, this value is made false.
|
||||||
|
* Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
|
||||||
|
*/
|
||||||
|
private[support] var lfsm: Boolean = false
|
||||||
|
private[support] var squadSetup: () => Unit = FirstTimeSquadSetup
|
||||||
|
private var squadUpdateCounter: Int = 0
|
||||||
|
private val queuedSquadActions: Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
|
||||||
|
private[support] var updateSquad: () => Unit = NoSquadUpdates
|
||||||
|
private var updateSquadRef: ActorRef = Default.Actor
|
||||||
|
|
||||||
|
/* packet */
|
||||||
|
|
||||||
|
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
|
||||||
|
val SquadDefinitionActionMessage(u1, u2, action) = pkt
|
||||||
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
|
||||||
|
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 = {
|
||||||
|
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 (
|
||||||
|
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)
|
||||||
|
squad_supplement_id = squad.GUID.guid + 1
|
||||||
|
membershipPositions.foreach {
|
||||||
|
case (member, index) =>
|
||||||
|
sendResponse(
|
||||||
|
SquadMemberEvent.Add(
|
||||||
|
squad_supplement_id,
|
||||||
|
member.CharId,
|
||||||
|
index,
|
||||||
|
member.Name,
|
||||||
|
member.ZoneId,
|
||||||
|
outfit_id = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
squadUI(member.CharId) =
|
||||||
|
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
||||||
|
}
|
||||||
|
//repeat our entry
|
||||||
|
sendResponse(
|
||||||
|
SquadMemberEvent.Add(
|
||||||
|
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
|
||||||
|
GiveSquadColorsToMembers()
|
||||||
|
GiveSquadColorsForOthers(playerGuid, factionChannel, 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())
|
||||||
|
updateSquadRef = ref
|
||||||
|
updateSquad = PeriodicUpdatesWhenEnrolledInSquad
|
||||||
|
chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
|
||||||
|
case _ =>
|
||||||
|
//other player is joining our squad
|
||||||
|
//load each member's entry
|
||||||
|
GiveSquadColorsToMembers(
|
||||||
|
membershipPositions.map {
|
||||||
|
case (member, index) =>
|
||||||
|
val charId = member.CharId
|
||||||
|
sendResponse(
|
||||||
|
SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
|
||||||
|
)
|
||||||
|
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(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)
|
||||||
|
updateSquadRef = Default.Actor
|
||||||
|
positionsToUpdate.foreach {
|
||||||
|
case (member, index) =>
|
||||||
|
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
|
||||||
|
squadUI.remove(member)
|
||||||
|
}
|
||||||
|
//uninitialize
|
||||||
|
val playerGuid = player.GUID
|
||||||
|
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
|
||||||
|
GiveSquadColorsToSelf(value = 0)
|
||||||
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
|
||||||
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
|
||||||
|
lfsm = false
|
||||||
|
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||||
|
//a finalization? what does this do?
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
|
||||||
|
squad_supplement_id = 0
|
||||||
|
squadUpdateCounter = 0
|
||||||
|
updateSquad = NoSquadUpdates
|
||||||
|
chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
|
||||||
|
case _ =>
|
||||||
|
//remove each member's entry
|
||||||
|
GiveSquadColorsToMembers(
|
||||||
|
positionsToUpdate.map {
|
||||||
|
case (member, index) =>
|
||||||
|
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
|
||||||
|
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
|
||||||
|
SwapSquadUIElements(squad, from_index, to_index)
|
||||||
|
|
||||||
|
case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
|
||||||
|
if (promotedPlayer != player.CharId) {
|
||||||
|
//demoted from leader; no longer lfsm
|
||||||
|
if (lfsm) {
|
||||||
|
lfsm = false
|
||||||
|
AvatarActor.displayLookingForSquad(session, state = 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
|
||||||
|
//the players have already been swapped in the backend object
|
||||||
|
PromoteSquadUIElements(squad, from_index)
|
||||||
|
|
||||||
|
case SquadResponse.UpdateMembers(_, positions) =>
|
||||||
|
val pairedEntries = positions.collect {
|
||||||
|
case entry if squadUI.contains(entry.char_id) =>
|
||||||
|
(entry, 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(squad_supplement_id, entry.char_id, element.index, entry.zone_number)
|
||||||
|
)
|
||||||
|
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
|
||||||
|
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(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(
|
||||||
|
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(
|
||||||
|
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(squad_supplement_id, char_id, waypoint_type))
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
|
||||||
|
* While many of these messages will be reused for other situations, they appear in this order only during startup.
|
||||||
|
*/
|
||||||
|
def FirstTimeSquadSetup(): Unit = {
|
||||||
|
sendResponse(SquadDetailDefinitionUpdateMessage.Init)
|
||||||
|
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
|
||||||
|
//only need to load these once - they persist between zone transfers and respawns
|
||||||
|
avatar.loadouts.squad.zipWithIndex.foreach {
|
||||||
|
case (Some(loadout), index) =>
|
||||||
|
sendResponse(
|
||||||
|
SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task))
|
||||||
|
)
|
||||||
|
case (None, _) => ;
|
||||||
|
}
|
||||||
|
//non-squad GUID-0 counts as the settings when not joined with a squad
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader()))
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad()))
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
|
||||||
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
|
||||||
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId())
|
||||||
|
squadSetup = RespawnSquadSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
|
||||||
|
* By using `squadUI` to maintain relevant information about squad members,
|
||||||
|
* especially the unique character identifier number,
|
||||||
|
* only the zone-specific squad members will receive the important messages about their squad member's spawn.
|
||||||
|
*/
|
||||||
|
def RespawnSquadSetup(): Unit = {
|
||||||
|
if (squad_supplement_id > 0) {
|
||||||
|
squadUI.get(player.CharId) match {
|
||||||
|
case Some(elem) =>
|
||||||
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
|
||||||
|
continent.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
s"${player.Faction}",
|
||||||
|
AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id)
|
||||||
|
)
|
||||||
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, elem.index))
|
||||||
|
case _ =>
|
||||||
|
log.warn(s"RespawnSquadSetup: asked to redraw squad information, but ${player.Name} has no squad element for squad $squad_supplement_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
|
||||||
|
* During a zone change,
|
||||||
|
* on top of other squad mates in the zone needing to have their knowledge of this player's squad colors changed,
|
||||||
|
* the player must also set squad colors for each other squad members.
|
||||||
|
* Default respawn functionality may resume afterwards.
|
||||||
|
*/
|
||||||
|
def ZoneChangeSquadSetup(): Unit = {
|
||||||
|
RespawnSquadSetup()
|
||||||
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
|
||||||
|
GiveSquadColorsInZone()
|
||||||
|
squadSetup = RespawnSquadSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
def NoSquadUpdates(): Unit = {}
|
||||||
|
|
||||||
|
def SquadUpdates(): Unit = {
|
||||||
|
updateSquadRef ! SquadServiceMessage(
|
||||||
|
player,
|
||||||
|
continent,
|
||||||
|
SquadServiceAction.Update(
|
||||||
|
player.CharId,
|
||||||
|
player.GUID,
|
||||||
|
player.Health,
|
||||||
|
player.MaxHealth,
|
||||||
|
player.Armor,
|
||||||
|
player.MaxArmor,
|
||||||
|
player.avatar.certifications,
|
||||||
|
player.Position,
|
||||||
|
continent.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def PeriodicUpdatesWhenEnrolledInSquad(): Unit = {
|
||||||
|
queuedSquadActions(squadUpdateCounter)()
|
||||||
|
squadUpdateCounter = (squadUpdateCounter + 1) % queuedSquadActions.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsInZone(): Unit = {
|
||||||
|
GiveSquadColorsInZone(squadUI.keys, squad_supplement_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
*
|
||||||
|
* @param members members of the squad to target
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsInZone(members: Iterable[Long]): Unit = {
|
||||||
|
GiveSquadColorsInZone(members, squad_supplement_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
*
|
||||||
|
* @see `PlanetsideAttributeMessage`
|
||||||
|
* @param members members of the squad to target
|
||||||
|
* @param value the assignment value
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsInZone(members: Iterable[Long], value: Long): Unit = {
|
||||||
|
SquadMembersInZone(members).foreach { members =>
|
||||||
|
sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the listed squad member unique character identifier numbers,
|
||||||
|
* find and return all squad members in the current zone.
|
||||||
|
*
|
||||||
|
* @param members members of the squad to target
|
||||||
|
* @return a list of `Player` objects
|
||||||
|
*/
|
||||||
|
def SquadMembersInZone(members: Iterable[Long]): Iterable[Player] = {
|
||||||
|
val players = continent.LivePlayers
|
||||||
|
for {
|
||||||
|
charId <- members
|
||||||
|
player = players.find {
|
||||||
|
_.CharId == charId
|
||||||
|
}
|
||||||
|
if player.nonEmpty
|
||||||
|
} yield player.get
|
||||||
|
}
|
||||||
|
|
||||||
|
def SwapSquadUIElements(squad: Squad, fromIndex: Int, toIndex: Int): Unit = {
|
||||||
|
if (squadUI.nonEmpty) {
|
||||||
|
val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object
|
||||||
|
val fromCharId = fromMember.CharId
|
||||||
|
val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object
|
||||||
|
val toCharId = toMember.CharId
|
||||||
|
val id = 11
|
||||||
|
if (toCharId > 0) {
|
||||||
|
//toMember and fromMember have swapped places
|
||||||
|
val fromElem = squadUI(fromCharId)
|
||||||
|
val toElem = squadUI(toCharId)
|
||||||
|
squadUI(toCharId) =
|
||||||
|
SquadUIElement(fromElem.name, fromElem.outfit, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
|
||||||
|
squadUI(fromCharId) =
|
||||||
|
SquadUIElement(toElem.name, toElem.outfit, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position)
|
||||||
|
sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, outfit_id = 0))
|
||||||
|
sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, outfit_id = 0))
|
||||||
|
sendResponse(
|
||||||
|
SquadState(
|
||||||
|
PlanetSideGUID(id),
|
||||||
|
List(
|
||||||
|
SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, unk6=false, 429, None, None),
|
||||||
|
SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, unk6=false, 429, None, None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
//previous fromMember has moved toMember
|
||||||
|
val elem = squadUI(fromCharId)
|
||||||
|
squadUI(fromCharId) = SquadUIElement(elem.name, elem.outfit, toIndex, elem.zone, elem.health, elem.armor, elem.position)
|
||||||
|
sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex))
|
||||||
|
sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, outfit_id = 0))
|
||||||
|
sendResponse(
|
||||||
|
SquadState(
|
||||||
|
PlanetSideGUID(id),
|
||||||
|
List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, unk6=false, 429, None, None))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val charId = avatar.id
|
||||||
|
if (toCharId == charId) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
|
||||||
|
} else if (fromCharId == charId) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give the squad colors associated with the current squad to the client's player character.
|
||||||
|
* @param value value to associate the player
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsToSelf(value: Long): Unit = {
|
||||||
|
GiveSquadColorsToSelf(player.GUID, player.Faction, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give the squad colors associated with the current squad to the client's player character.
|
||||||
|
* @param guid player guid
|
||||||
|
* @param faction faction for targeted updates to other players
|
||||||
|
* @param value value to associate the player
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsToSelf(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(guid, 31, value))
|
||||||
|
GiveSquadColorsForOthers(guid, faction, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give the squad colors associated with the current squad to the client's player character.
|
||||||
|
* @param guid player guid
|
||||||
|
* @param faction faction for targeted updates to other players
|
||||||
|
* @param value value to associate the player
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsForOthers(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = {
|
||||||
|
GiveSquadColorsForOthers(guid, faction.toString, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give the squad colors associated with the current squad to the client's player character to other players.
|
||||||
|
* @param guid player guid
|
||||||
|
* @param factionChannel faction for targeted updates to other players
|
||||||
|
* @param value value to associate the player
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsForOthers(guid: PlanetSideGUID, factionChannel: String, value: Long): Unit = {
|
||||||
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(guid, 31, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsToMembers(): Unit = {
|
||||||
|
GiveSquadColorsToMembers(squadUI.keys, squad_supplement_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
* @param members members of the squad to target
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsToMembers(members: Iterable[Long]): Unit = {
|
||||||
|
GiveSquadColorsToMembers(members, squad_supplement_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
||||||
|
* @see `PlanetsideAttributeMessage`
|
||||||
|
* @param members members of the squad to target
|
||||||
|
* @param value the assignment value
|
||||||
|
*/
|
||||||
|
def GiveSquadColorsToMembers(members: Iterable[Long], value: Long): Unit = {
|
||||||
|
SquadMembersInZone(members).foreach { members =>
|
||||||
|
sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def PromoteSquadUIElements(squad: Squad, fromIndex: Int): Unit = {
|
||||||
|
//the players should have already been swapped in the backend object
|
||||||
|
val firstMember = squad.Membership(0)
|
||||||
|
val firstCharId = firstMember.CharId
|
||||||
|
val secondMember = squad.Membership(fromIndex)
|
||||||
|
val secondCharId = secondMember.CharId
|
||||||
|
if (squadUI.nonEmpty && fromIndex != 0 && firstCharId > 0 && secondCharId > 0) {
|
||||||
|
val newFirstElem = squadUI(firstCharId).copy(index = 0)
|
||||||
|
val newSecondElem = squadUI(secondCharId).copy(index = fromIndex)
|
||||||
|
val charId = player.CharId
|
||||||
|
val pguid = player.GUID
|
||||||
|
val sguid = squad.GUID
|
||||||
|
val id = squad_supplement_id
|
||||||
|
//secondMember and firstMember swap places
|
||||||
|
squadUI.put(firstCharId, newFirstElem)
|
||||||
|
squadUI.put(secondCharId, newSecondElem)
|
||||||
|
sendResponse(SquadMemberEvent(MemberEvent.Promote, id, firstCharId, position = 0))
|
||||||
|
//player is being either promoted or demoted?
|
||||||
|
if (firstCharId == charId) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(pguid, 32, 0))
|
||||||
|
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.IdentifyAsSquadLeader()))
|
||||||
|
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
|
||||||
|
} else if (secondCharId == charId) {
|
||||||
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader()))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(pguid, 32, fromIndex))
|
||||||
|
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
|
||||||
|
}
|
||||||
|
//seed updates (just for the swapped players)
|
||||||
|
sendResponse(
|
||||||
|
SquadState(PlanetSideGUID(id), List(
|
||||||
|
SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position),
|
||||||
|
SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import scala.concurrent.Future
|
||||||
|
//
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
|
||||||
|
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
|
||||||
|
import net.psforever.objects.guid.{StraightforwardTask, TaskBundle, TaskWorkflow}
|
||||||
|
import net.psforever.objects.PlanetSideGameObject
|
||||||
|
import net.psforever.objects.equipment.EffectTarget
|
||||||
|
import net.psforever.objects.serverobject.CommonMessages
|
||||||
|
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||||
|
import net.psforever.objects.serverobject.terminals.{ProximityDefinition, ProximityUnit, Terminal}
|
||||||
|
import net.psforever.packet.game.{ItemTransactionMessage, ItemTransactionResultMessage,ProximityTerminalUseMessage, UnuseItemMessage}
|
||||||
|
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
|
||||||
|
|
||||||
|
class SessionTerminalHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
private[support] var lastTerminalOrderFulfillment: Boolean = true
|
||||||
|
private[support] var usingMedicalTerminal: Option[PlanetSideGUID] = None
|
||||||
|
|
||||||
|
/* packets */
|
||||||
|
|
||||||
|
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
|
||||||
|
val ItemTransactionMessage(terminalGuid, _, _, _, _, _) = pkt
|
||||||
|
continent.GUID(terminalGuid) match {
|
||||||
|
case Some(term: Terminal) =>
|
||||||
|
if (lastTerminalOrderFulfillment) {
|
||||||
|
log.trace(s"ItemTransactionMessage: ${player.Name} is submitting an order")
|
||||||
|
lastTerminalOrderFulfillment = false
|
||||||
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||||
|
term.Actor ! Terminal.Request(player, pkt)
|
||||||
|
}
|
||||||
|
case Some(obj: PlanetSideGameObject) =>
|
||||||
|
log.error(s"ItemTransaction: $obj is not a terminal, ${player.Name}")
|
||||||
|
case _ =>
|
||||||
|
log.error(s"ItemTransaction: $terminalGuid does not exist, ${player.Name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
|
||||||
|
val ProximityTerminalUseMessage(_, object_guid, _) = pkt
|
||||||
|
continent.GUID(object_guid) match {
|
||||||
|
case Some(obj: Terminal with ProximityUnit) =>
|
||||||
|
HandleProximityTerminalUse(obj)
|
||||||
|
case Some(obj) =>
|
||||||
|
log.warn(s"ProximityTerminalUse: $obj does not have proximity effects for ${player.Name}")
|
||||||
|
case None =>
|
||||||
|
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid $object_guid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* response handler */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) =>
|
||||||
|
tplayer.avatar.purchaseCooldown(item.Definition) match {
|
||||||
|
case Some(_) =>
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||||
|
case None =>
|
||||||
|
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
|
||||||
|
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)
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case Terminal.SellCertification(cert) =>
|
||||||
|
avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case Terminal.LearnImplant(implant) =>
|
||||||
|
avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case Terminal.SellImplant(implant) =>
|
||||||
|
avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
|
||||||
|
tplayer.avatar.purchaseCooldown(vehicle.Definition) match {
|
||||||
|
case Some(_) =>
|
||||||
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||||
|
case None =>
|
||||||
|
continent.map.terminalToSpawnPad
|
||||||
|
.find { case (termid, _) => termid == msg.terminal_guid.guid }
|
||||||
|
.collect {
|
||||||
|
case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b))
|
||||||
|
case _ => (None, None)
|
||||||
|
}
|
||||||
|
.get match {
|
||||||
|
case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
|
||||||
|
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(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))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case Terminal.NoDeal() =>
|
||||||
|
val order: String = if (msg == null) {
|
||||||
|
"missing order"
|
||||||
|
} else {
|
||||||
|
s"${msg.transaction_type} order"
|
||||||
|
}
|
||||||
|
log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the $order")
|
||||||
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, success = false))
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
val transaction = msg.transaction_type
|
||||||
|
log.warn(s"n/a: ${tplayer.Name} made a $transaction request but terminal#${msg.terminal_guid.guid} is missing or wrong")
|
||||||
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
|
||||||
|
lastTerminalOrderFulfillment = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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`.
|
||||||
|
* Before being finished, that vehicle is supplied an `Actor` such that it may function properly.
|
||||||
|
* This function wraps around `RegisterVehicle` and is used in case, prior to this event,
|
||||||
|
* the vehicle is being brought into existence from scratch and was never a member of any `Zone`.
|
||||||
|
* @param vehicle the `Vehicle` object
|
||||||
|
* @see `RegisterVehicle`
|
||||||
|
* @return a `TaskBundle` message
|
||||||
|
*/
|
||||||
|
private[session] def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = {
|
||||||
|
TaskBundle(
|
||||||
|
new StraightforwardTask() {
|
||||||
|
private val localVehicle = vehicle
|
||||||
|
private val localPad = pad.Actor
|
||||||
|
private val localTerminal = terminal
|
||||||
|
private val localPlayer = player
|
||||||
|
|
||||||
|
override def description(): String = s"register a ${localVehicle.Definition.Name} for spawn pad"
|
||||||
|
|
||||||
|
def action(): Future[Any] = {
|
||||||
|
localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle, localTerminal)
|
||||||
|
Future(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
List(sessionData.registerVehicle(vehicle))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param terminal na
|
||||||
|
*/
|
||||||
|
def HandleProximityTerminalUse(terminal: Terminal with ProximityUnit): Unit = {
|
||||||
|
val term_guid = terminal.GUID
|
||||||
|
val targets = FindProximityUnitTargetsInScope(terminal)
|
||||||
|
val currentTargets = terminal.Targets
|
||||||
|
targets.foreach(target => {
|
||||||
|
if (!currentTargets.contains(target)) {
|
||||||
|
StartUsingProximityUnit(terminal, target)
|
||||||
|
} else if (targets.isEmpty) {
|
||||||
|
log.warn(
|
||||||
|
s"HandleProximityTerminalUse: ${player.Name} could not find valid targets to give to proximity unit ${terminal.Definition.Name}@${term_guid.guid}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
* @param terminal na
|
||||||
|
* @return na
|
||||||
|
*/
|
||||||
|
def FindProximityUnitTargetsInScope(terminal: Terminal with ProximityUnit): Seq[PlanetSideGameObject] = {
|
||||||
|
terminal.Definition.asInstanceOf[ProximityDefinition].TargetValidation.keySet collect {
|
||||||
|
case EffectTarget.Category.Player => Some(player)
|
||||||
|
case EffectTarget.Category.Vehicle | EffectTarget.Category.Aircraft => continent.GUID(player.VehicleSeated)
|
||||||
|
} collect {
|
||||||
|
case Some(a) => a
|
||||||
|
} toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a proximity-based service.
|
||||||
|
* @param terminal the proximity-based unit
|
||||||
|
* @param target the entity that is being considered for terminal operation
|
||||||
|
*/
|
||||||
|
def StartUsingProximityUnit(terminal: Terminal with ProximityUnit, target: PlanetSideGameObject): Unit = {
|
||||||
|
val term_guid = terminal.GUID
|
||||||
|
//log.trace(s"StartUsingProximityUnit: ${player.Name} wants to use ${terminal.Definition.Name}@${term_guid.guid} on $target")
|
||||||
|
if (player.isAlive) {
|
||||||
|
target match {
|
||||||
|
case _: Player =>
|
||||||
|
terminal.Actor ! CommonMessages.Use(player, Some(target))
|
||||||
|
case _: Vehicle =>
|
||||||
|
terminal.Actor ! CommonMessages.Use(player, Some(target))
|
||||||
|
case _ =>
|
||||||
|
log.error(
|
||||||
|
s"StartUsingProximityUnit: ${player.Name}, this ${terminal.Definition.Name} can not deal with target $target"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
terminal.Definition match {
|
||||||
|
case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal =>
|
||||||
|
usingMedicalTerminal = Some(term_guid)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop using a proximity-base service.
|
||||||
|
* If the suggested terminal detects our player or our player's vehicle as a valid target for its effect,
|
||||||
|
* inform it that we wish it stop affecting the discovered target(s).
|
||||||
|
* @param terminal the proximity-based unit
|
||||||
|
*/
|
||||||
|
def StopUsingProximityUnit(terminal: Terminal with ProximityUnit): Unit = {
|
||||||
|
FindProximityUnitTargetsInScope(terminal).foreach { target =>
|
||||||
|
LocalStopUsingProximityUnit(terminal, target)
|
||||||
|
terminal.Actor ! CommonMessages.Unuse(player, Some(target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop using a proximity-base service.
|
||||||
|
* Callback to handle flags specific to `SessionActor`.
|
||||||
|
* Special note is warranted when determining the identity of the proximity terminal.
|
||||||
|
* Medical terminals of both varieties can be cancelled by movement.
|
||||||
|
* Other sorts of proximity-based units are put on a timer.
|
||||||
|
* @param terminal the proximity-based unit
|
||||||
|
*/
|
||||||
|
def LocalStopUsingProximityUnit(terminal: Terminal with ProximityUnit, target: PlanetSideGameObject): Unit = {
|
||||||
|
val term_guid = terminal.GUID
|
||||||
|
if (usingMedicalTerminal.contains(term_guid)) {
|
||||||
|
usingMedicalTerminal = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cease all current interactions with proximity-based units.
|
||||||
|
* Pair with `PlayerActionsToCancel`, except when logging out (stopping).
|
||||||
|
* This operations may invoke callback messages.
|
||||||
|
* @see `postStop`
|
||||||
|
*/
|
||||||
|
def CancelAllProximityUnits(): Unit = {
|
||||||
|
continent.GUID(usingMedicalTerminal) match {
|
||||||
|
case Some(terminal: Terminal with ProximityUnit) =>
|
||||||
|
FindProximityUnitTargetsInScope(terminal).foreach(target =>
|
||||||
|
terminal.Actor ! CommonMessages.Unuse(player, Some(target))
|
||||||
|
)
|
||||||
|
ForgetAllProximityTerminals(usingMedicalTerminal.get)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
*/
|
||||||
|
def ForgetAllProximityTerminals(term_guid: PlanetSideGUID): Unit = {
|
||||||
|
if (usingMedicalTerminal.contains(term_guid)) {
|
||||||
|
usingMedicalTerminal = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, ActorRef, typed}
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
|
||||||
|
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.{GlobalDefinitions, Player, Vehicle, Vehicles}
|
||||||
|
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||||
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.services.Service
|
||||||
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
|
||||||
|
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
class SessionVehicleHandlers(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
galaxyService: ActorRef,
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
/**
|
||||||
|
* na
|
||||||
|
*
|
||||||
|
* @param toChannel na
|
||||||
|
* @param guid na
|
||||||
|
* @param reply na
|
||||||
|
*/
|
||||||
|
def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = {
|
||||||
|
val tplayer_guid = if (player.HasGUID) player.GUID else PlanetSideGUID(0)
|
||||||
|
reply match {
|
||||||
|
case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) =>
|
||||||
|
sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3))
|
||||||
|
|
||||||
|
case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.ConcealPlayer(player_guid) =>
|
||||||
|
sendResponse(GenericObjectActionMessage(player_guid, 9))
|
||||||
|
|
||||||
|
case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.DeployRequest(object_guid, state, unk1, unk2, pos) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) =>
|
||||||
|
val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad].Definition
|
||||||
|
sendResponse(
|
||||||
|
ObjectDetachMessage(
|
||||||
|
pad_guid,
|
||||||
|
vehicle_guid,
|
||||||
|
pad_position + Vector3.z(pad.VehicleCreationZOffset),
|
||||||
|
pad_orientation_z + pad.VehicleCreationZOrientOffset
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
case VehicleResponse.EquipmentInSlot(pkt) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.GenericObjectAction(object_guid, action) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(GenericObjectActionMessage(object_guid, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.HitHint(source_guid) =>
|
||||||
|
if (player.isAlive) {
|
||||||
|
sendResponse(HitHint(source_guid, player.GUID))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
|
||||||
|
val obj_guid = obj.GUID
|
||||||
|
sendResponse(ObjectDeleteMessage(obj_guid, 0))
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateDetailedMessage(
|
||||||
|
obj.Definition.ObjectId,
|
||||||
|
obj_guid,
|
||||||
|
ObjectCreateMessageParent(parent_guid, start),
|
||||||
|
con_data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicle_guid) =>
|
||||||
|
//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
|
||||||
|
sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
|
||||||
|
if (tplayer_guid == guid) {
|
||||||
|
val typeOfRide = continent.GUID(vehicle_guid) match {
|
||||||
|
case Some(obj: Vehicle) =>
|
||||||
|
sessionData.UnaccessContainer(obj)
|
||||||
|
s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}"
|
||||||
|
case _ =>
|
||||||
|
s"${player.Sex.possessive} ride"
|
||||||
|
}
|
||||||
|
log.info(s"${player.Name} has been kicked from $typeOfRide!")
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(InventoryStateMessage(obj_guid, 0, parent_guid, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) =>
|
||||||
|
//this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
|
||||||
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.MountVehicle(vehicle_guid, seat) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.Ownership(vehicleGuid) =>
|
||||||
|
if (tplayer_guid == guid) { // Only the player that owns this vehicle needs the ownership packet
|
||||||
|
avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
|
||||||
|
sendResponse(PlanetsideAttributeMessage(tplayer_guid, 21, vehicleGuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.ResetSpawnPad(pad_guid) =>
|
||||||
|
sendResponse(GenericObjectActionMessage(pad_guid, 23))
|
||||||
|
|
||||||
|
case VehicleResponse.RevealPlayer(player_guid) =>
|
||||||
|
sendResponse(GenericObjectActionMessage(player_guid, 10))
|
||||||
|
|
||||||
|
case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.StowEquipment(vehicle_guid, slot, item_type, item_guid, item_data) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
//TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
|
||||||
|
sendResponse(
|
||||||
|
ObjectCreateDetailedMessage(item_type, item_guid, ObjectCreateMessageParent(vehicle_guid, slot), item_data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.UnloadVehicle(_, vehicle_guid) =>
|
||||||
|
sendResponse(ObjectDeleteMessage(vehicle_guid, 0))
|
||||||
|
|
||||||
|
case VehicleResponse.UnstowEquipment(item_guid) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
|
||||||
|
sendResponse(ObjectDeleteMessage(item_guid, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.VehicleState(
|
||||||
|
vehicle_guid,
|
||||||
|
unk1,
|
||||||
|
pos,
|
||||||
|
ang,
|
||||||
|
vel,
|
||||||
|
unk2,
|
||||||
|
unk3,
|
||||||
|
unk4,
|
||||||
|
wheel_direction,
|
||||||
|
unk5,
|
||||||
|
unk6
|
||||||
|
) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sendResponse(
|
||||||
|
VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6)
|
||||||
|
)
|
||||||
|
if (player.VehicleSeated.contains(vehicle_guid)) {
|
||||||
|
player.Position = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case VehicleResponse.SendResponse(msg) =>
|
||||||
|
sendResponse(msg)
|
||||||
|
|
||||||
|
case VehicleResponse.UpdateAmsSpawnPoint(list) =>
|
||||||
|
sessionData.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
|
||||||
|
sessionData.zoning.spawn.DrawCurrentAmsSpawnPoint()
|
||||||
|
|
||||||
|
case VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle, vehicle_to_delete) =>
|
||||||
|
if (tplayer_guid != guid) {
|
||||||
|
sessionData.zoning.interstellarFerry = Some(vehicle)
|
||||||
|
sessionData.zoning.interstellarFerryTopLevelGUID = Some(vehicle_to_delete)
|
||||||
|
continent.VehicleEvents ! Service.Leave(
|
||||||
|
Some(old_channel)
|
||||||
|
) //old vehicle-specific channel (was s"${vehicle.Actor}")
|
||||||
|
galaxyService ! Service.Join(temp_channel) //temporary vehicle-specific channel
|
||||||
|
log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $temp_channel for vehicle gating")
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.KickCargo(vehicle, speed, delay) =>
|
||||||
|
if (player.VehicleSeated.nonEmpty && sessionData.zoning.spawn.deadState == DeadState.Alive) {
|
||||||
|
if (speed > 0) {
|
||||||
|
val strafe =
|
||||||
|
if (Vehicles.CargoOrientation(vehicle) == 1) 2
|
||||||
|
else 1
|
||||||
|
val reverseSpeed =
|
||||||
|
if (strafe > 1) 0
|
||||||
|
else speed
|
||||||
|
//strafe or reverse, not both
|
||||||
|
sessionData.vehicles.serverVehicleControlVelocity = Some(reverseSpeed)
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=true, lock_wheel=true, reverse=true, unk4=false, 0, strafe, reverseSpeed, Some(0)))
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
context.system.scheduler.scheduleOnce(
|
||||||
|
delay milliseconds,
|
||||||
|
context.self,
|
||||||
|
VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, 0, delay))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sessionData.vehicles.serverVehicleControlVelocity = None
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false,lock_wheel=false, reverse=false, unk4=false, 0, 0, 0, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
|
||||||
|
val vehicle_guid = vehicle.GUID
|
||||||
|
sessionData.PlayerActionsToCancel()
|
||||||
|
sessionData.vehicles.serverVehicleControlVelocity = Some(0)
|
||||||
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
|
if (player.VisibleSlots.contains(player.DrawnSlot)) {
|
||||||
|
player.DrawnSlot = Player.HandsDownSlot
|
||||||
|
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1 = true))
|
||||||
|
continent.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
continent.id,
|
||||||
|
AvatarAction.SendResponse(player.GUID, ObjectHeldMessage(player.GUID, player.LastDrawnSlot, unk1 = false))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off
|
||||||
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 21, vehicle_guid)) //ownership
|
||||||
|
vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 } match {
|
||||||
|
case Some((mountPoint, _)) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
|
||||||
|
val vehicle_guid = vehicle.GUID
|
||||||
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on
|
||||||
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
||||||
|
sessionData.vehicles.ServerVehicleLock(vehicle)
|
||||||
|
|
||||||
|
case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
|
||||||
|
val vdef = vehicle.Definition
|
||||||
|
sessionData.vehicles.ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, if (GlobalDefinitions.isFlightVehicle(vdef)) 1 else 0)
|
||||||
|
|
||||||
|
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
|
||||||
|
session = session.copy(avatar = avatar.copy(vehicle = Some(vehicle.GUID)))
|
||||||
|
sessionData.vehicles.DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
|
||||||
|
|
||||||
|
case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
|
||||||
|
sendResponse(ChatMsg(
|
||||||
|
ChatMessageType.CMT_OPEN,
|
||||||
|
wideContents=true,
|
||||||
|
"",
|
||||||
|
s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
|
||||||
|
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, "", msg, None))
|
||||||
|
|
||||||
|
case VehicleResponse.ChangeLoadout(target, old_weapons, added_weapons, old_inventory, new_inventory) =>
|
||||||
|
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
|
||||||
|
continent.GUID(target) match {
|
||||||
|
case Some(vehicle: Vehicle) =>
|
||||||
|
if (player.avatar.vehicle.contains(target)) {
|
||||||
|
import net.psforever.login.WorldSession.boolToInt
|
||||||
|
//owner: must unregister old equipment, and register and install new equipment
|
||||||
|
(old_weapons ++ old_inventory).foreach {
|
||||||
|
case (obj, eguid) =>
|
||||||
|
sendResponse(ObjectDeleteMessage(eguid, 0))
|
||||||
|
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||||
|
}
|
||||||
|
sessionData.ApplyPurchaseTimersBeforePackingLoadout(player, vehicle, added_weapons ++ new_inventory)
|
||||||
|
//jammer or unjamm new weapons based on vehicle status
|
||||||
|
val vehicleJammered = vehicle.Jammed
|
||||||
|
added_weapons
|
||||||
|
.map {
|
||||||
|
_.obj
|
||||||
|
}
|
||||||
|
.collect {
|
||||||
|
case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
|
||||||
|
jamItem.Jammed = vehicleJammered
|
||||||
|
JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
|
||||||
|
}
|
||||||
|
} else if (sessionData.accessedContainer.map { _.GUID }.contains(target)) {
|
||||||
|
//external participant: observe changes to equipment
|
||||||
|
(old_weapons ++ old_inventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) }
|
||||||
|
}
|
||||||
|
vehicle.PassengerInSeat(player) match {
|
||||||
|
case Some(seatNum) =>
|
||||||
|
//participant: observe changes to equipment
|
||||||
|
(old_weapons ++ old_inventory).foreach {
|
||||||
|
case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0))
|
||||||
|
}
|
||||||
|
sessionData.UpdateWeaponAtSeatPosition(vehicle, seatNum)
|
||||||
|
case None =>
|
||||||
|
//observer: observe changes to external equipment
|
||||||
|
old_weapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) }
|
||||||
|
}
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,658 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
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.vehicles.{AccessPermissionGroup, CargoBehavior}
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.objects._
|
||||||
|
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||||
|
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, MountVehicleCargoMsg, MountVehicleMsg, VehicleSubStateMessage, _}
|
||||||
|
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||||
|
import net.psforever.types.{BailType, DriveState, Vector3}
|
||||||
|
|
||||||
|
class VehicleOperations(
|
||||||
|
val sessionData: SessionData,
|
||||||
|
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||||
|
implicit val context: ActorContext
|
||||||
|
) extends CommonSessionInterfacingFunctionality {
|
||||||
|
private[support] var serverVehicleControlVelocity: Option[Int] = None
|
||||||
|
|
||||||
|
/* packets */
|
||||||
|
|
||||||
|
def handleVehicleState(pkt: VehicleStateMessage): Unit = {
|
||||||
|
val VehicleStateMessage(
|
||||||
|
vehicle_guid,
|
||||||
|
unk1,
|
||||||
|
pos,
|
||||||
|
ang,
|
||||||
|
vel,
|
||||||
|
is_flying,
|
||||||
|
unk6,
|
||||||
|
unk7,
|
||||||
|
wheels,
|
||||||
|
is_decelerating,
|
||||||
|
is_cloaked
|
||||||
|
) = pkt
|
||||||
|
GetVehicleAndSeat() match {
|
||||||
|
case (Some(obj), Some(0)) =>
|
||||||
|
//we're driving the vehicle
|
||||||
|
sessionData.persist()
|
||||||
|
sessionData.turnCounterFunc(player.GUID)
|
||||||
|
sessionData.fallHeightTracker(pos.z)
|
||||||
|
if (obj.MountedIn.isEmpty) {
|
||||||
|
sessionData.updateBlockMap(obj, continent, 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sessionData.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) {
|
||||||
|
sessionData.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
|
||||||
|
GetVehicleAndSeat() match {
|
||||||
|
case (Some(obj), Some(0)) =>
|
||||||
|
//we're driving the vehicle
|
||||||
|
sessionData.persist()
|
||||||
|
sessionData.turnCounterFunc(player.GUID)
|
||||||
|
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||||
|
case Some(v: Vehicle) =>
|
||||||
|
sessionData.updateBlockMap(obj, continent, 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sessionData.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) {
|
||||||
|
sessionData.KickedByAdministration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
|
||||||
|
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
|
||||||
|
val (o, tools) = sessionData.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 _ =>
|
||||||
|
sessionData.persist()
|
||||||
|
sessionData.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) {
|
||||||
|
sessionData.KickedByAdministration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
|
||||||
|
val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
|
||||||
|
sessionData.ValidObject(vehicle_guid, decorator = "VehicleSubState") match {
|
||||||
|
case Some(obj: Vehicle) =>
|
||||||
|
import net.psforever.login.WorldSession.boolToInt
|
||||||
|
obj.Position = pos
|
||||||
|
obj.Orientation = ang
|
||||||
|
obj.Velocity = vel
|
||||||
|
sessionData.updateBlockMap(obj, continent, 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
|
||||||
|
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
|
||||||
|
sessionData.ValidObject(mountable_guid, decorator = "MountVehicle") match {
|
||||||
|
case Some(obj: Mountable) =>
|
||||||
|
obj.Actor ! Mountable.TryMount(player, entry_point)
|
||||||
|
case Some(_) =>
|
||||||
|
log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
|
||||||
|
case None => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
|
||||||
|
val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
|
||||||
|
//TODO optimize this later
|
||||||
|
//common warning for this section
|
||||||
|
def dismountWarning(note: String): Unit = {
|
||||||
|
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
|
||||||
|
}
|
||||||
|
if (player.GUID == player_guid) {
|
||||||
|
//normally disembarking from a mount
|
||||||
|
(sessionData.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 _ =>
|
||||||
|
dismountWarning(
|
||||||
|
s"DismountVehicleMsg: player ${player.Name}_guid not considered seated in a mountable entity"
|
||||||
|
)
|
||||||
|
sendResponse(DismountVehicleMsg(player_guid, bailType, wasKickedByDriver))
|
||||||
|
None
|
||||||
|
}) match {
|
||||||
|
case Some(_) if serverVehicleControlVelocity.nonEmpty =>
|
||||||
|
log.debug(
|
||||||
|
s"DismountVehicleMsg: ${player.Name} can not dismount from vehicle while server has asserted control; please wait"
|
||||||
|
)
|
||||||
|
case Some(obj: Mountable) =>
|
||||||
|
obj.PassengerInSeat(player) match {
|
||||||
|
case Some(seat_num) =>
|
||||||
|
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
|
||||||
|
if (sessionData.zoning.interstellarFerry.isDefined) {
|
||||||
|
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
|
||||||
|
//see above in VehicleResponse.TransferPassenger case
|
||||||
|
sessionData.zoning.interstellarFerry = None
|
||||||
|
}
|
||||||
|
// Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
|
||||||
|
//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 =>
|
||||||
|
dismountWarning(
|
||||||
|
s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
dismountWarning(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//kicking someone else out of a mount; need to own that mount/mountable
|
||||||
|
player.avatar.vehicle match {
|
||||||
|
case Some(obj_guid) =>
|
||||||
|
(
|
||||||
|
(
|
||||||
|
sessionData.ValidObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
|
||||||
|
sessionData.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 =>
|
||||||
|
dismountWarning(
|
||||||
|
s"DismountVehicleMsg: can not find where other player ${player.Name}_guid is seated in mountable $obj_guid"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case (None, _) => ;
|
||||||
|
log.warn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle")
|
||||||
|
case (_, None) => ;
|
||||||
|
log.warn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}")
|
||||||
|
case _ =>
|
||||||
|
log.warn(s"DismountVehicleMsg: object is either not a Mountable or not a Player")
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
log.warn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _ => ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
|
||||||
|
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) =>
|
||||||
|
log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
|
||||||
|
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid")
|
||||||
|
avatarActor ! AvatarActor.SetVehicle(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(s"${player.Name} must be mounted to request a deployment change")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* messages */
|
||||||
|
|
||||||
|
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||||
|
if (state == DriveState.Deploying) {
|
||||||
|
log.trace(s"DeployRequest: $obj transitioning to deploy state")
|
||||||
|
} else if (state == DriveState.Deployed) {
|
||||||
|
log.trace(s"DeployRequest: $obj has been Deployed")
|
||||||
|
} else {
|
||||||
|
CanNotChangeDeployment(obj, state, "incorrect deploy state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||||
|
if (state == DriveState.Undeploying) {
|
||||||
|
log.trace(s"DeployRequest: $obj transitioning to undeploy state")
|
||||||
|
} else if (state == DriveState.Mobile) {
|
||||||
|
log.trace(s"DeployRequest: $obj is Mobile")
|
||||||
|
} else {
|
||||||
|
CanNotChangeDeployment(obj, state, "incorrect undeploy state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
|
||||||
|
if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) {
|
||||||
|
CanNotChangeDeployment(obj, state, reason = "ground too steep")
|
||||||
|
} else {
|
||||||
|
CanNotChangeDeployment(obj, state, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* support functions */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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`.
|
||||||
|
* Once an object is found, the remainder are ignored.
|
||||||
|
* @param direct a game object in which the player may be sat
|
||||||
|
* @param occupant the player who is sat and may have specified the game object in which mounted
|
||||||
|
* @return a tuple consisting of a vehicle reference and a mount index
|
||||||
|
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||||
|
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||||
|
*/
|
||||||
|
def GetMountableAndSeat(
|
||||||
|
direct: Option[PlanetSideGameObject with Mountable],
|
||||||
|
occupant: Player,
|
||||||
|
zone: Zone
|
||||||
|
): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
|
||||||
|
direct.orElse(zone.GUID(occupant.VehicleSeated)) match {
|
||||||
|
case Some(obj: PlanetSideGameObject with Mountable) =>
|
||||||
|
obj.PassengerInSeat(occupant) match {
|
||||||
|
case index @ Some(_) =>
|
||||||
|
(Some(obj), index)
|
||||||
|
case None =>
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.<br>
|
||||||
|
* <br>
|
||||||
|
* For special purposes involved in zone transfers,
|
||||||
|
* where the vehicle may or may not exist in either of the zones (yet),
|
||||||
|
* the value of `interstellarFerry` is also polled.
|
||||||
|
* Making certain this field is blanked after the transfer is completed is important
|
||||||
|
* to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed.
|
||||||
|
* @see `GetMountableAndSeat`
|
||||||
|
* @see `interstellarFerry`
|
||||||
|
* @return a tuple consisting of a vehicle reference and a mount index
|
||||||
|
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||||
|
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||||
|
*/
|
||||||
|
def GetKnownVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||||
|
GetMountableAndSeat(sessionData.zoning.interstellarFerry, player, continent) match {
|
||||||
|
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||||
|
case _ => (None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
|
||||||
|
* @see `GetMountableAndSeat`
|
||||||
|
* @return a tuple consisting of a vehicle reference and a mount index
|
||||||
|
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||||
|
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||||
|
*/
|
||||||
|
def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||||
|
GetMountableAndSeat(None, player, continent) match {
|
||||||
|
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||||
|
case _ => (None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to auto reverse them out
|
||||||
|
* Lock all applicable controls of the current vehicle
|
||||||
|
* Set the vehicle to move in reverse
|
||||||
|
*/
|
||||||
|
def ServerVehicleLockReverse(): Unit = {
|
||||||
|
serverVehicleControlVelocity = Some(-1)
|
||||||
|
sendResponse(
|
||||||
|
ServerVehicleOverrideMsg(
|
||||||
|
lock_accelerator = true,
|
||||||
|
lock_wheel = true,
|
||||||
|
reverse = true,
|
||||||
|
unk4 = true,
|
||||||
|
lock_vthrust = 0,
|
||||||
|
lock_strafe = 1,
|
||||||
|
movement_speed = 2,
|
||||||
|
unk8 = Some(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to strafe right out of the cargo hold for vehicles that are mounted sideways e.g. router/BFR
|
||||||
|
* Lock all applicable controls of the current vehicle
|
||||||
|
* Set the vehicle to strafe right
|
||||||
|
*/
|
||||||
|
def ServerVehicleLockStrafeRight(): Unit = {
|
||||||
|
serverVehicleControlVelocity = Some(-1)
|
||||||
|
sendResponse(
|
||||||
|
ServerVehicleOverrideMsg(
|
||||||
|
lock_accelerator = true,
|
||||||
|
lock_wheel = true,
|
||||||
|
reverse = false,
|
||||||
|
unk4 = true,
|
||||||
|
lock_vthrust = 0,
|
||||||
|
lock_strafe = 3,
|
||||||
|
movement_speed = 0,
|
||||||
|
unk8 = Some(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to strafe left out of the cargo hold for vehicles that are mounted sideways e.g. router/BFR
|
||||||
|
* Lock all applicable controls of the current vehicle
|
||||||
|
* Set the vehicle to strafe left
|
||||||
|
*/
|
||||||
|
def ServerVehicleLockStrafeLeft(): Unit = {
|
||||||
|
serverVehicleControlVelocity = Some(-1)
|
||||||
|
sendResponse(
|
||||||
|
ServerVehicleOverrideMsg(
|
||||||
|
lock_accelerator = true,
|
||||||
|
lock_wheel = true,
|
||||||
|
reverse = false,
|
||||||
|
unk4 = true,
|
||||||
|
lock_vthrust = 0,
|
||||||
|
lock_strafe = 2,
|
||||||
|
movement_speed = 0,
|
||||||
|
unk8 = Some(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock all applicable controls of the current vehicle.
|
||||||
|
* This includes forward motion, turning, and, if applicable, strafing.
|
||||||
|
* @param vehicle the vehicle being controlled
|
||||||
|
*/
|
||||||
|
def ServerVehicleLock(vehicle: Vehicle): Unit = {
|
||||||
|
serverVehicleControlVelocity = Some(-1)
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=true, lock_wheel=true, reverse=false, unk4=false, 0, 1, 0, Some(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place the current vehicle under the control of the server's commands.
|
||||||
|
* @param vehicle the vehicle
|
||||||
|
* @param speed how fast the vehicle is moving forward
|
||||||
|
* @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type
|
||||||
|
*/
|
||||||
|
def ServerVehicleOverride(vehicle: Vehicle, speed: Int = 0, flight: Int = 0): Unit = {
|
||||||
|
serverVehicleControlVelocity = Some(speed)
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=true, lock_wheel=true, reverse=false, unk4=false, flight, 0, speed, Some(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place the current vehicle under the control of the driver's commands,
|
||||||
|
* but leave it in a cancellable auto-drive.
|
||||||
|
* @param vehicle the vehicle
|
||||||
|
* @param speed how fast the vehicle is moving forward
|
||||||
|
* @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type
|
||||||
|
*/
|
||||||
|
def DriverVehicleControl(vehicle: Vehicle, speed: Int = 0, flight: Int = 0): Unit = {
|
||||||
|
if (serverVehicleControlVelocity.nonEmpty) {
|
||||||
|
serverVehicleControlVelocity = None
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false, lock_wheel=false, reverse=false, unk4=true, flight, 0, speed, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place the current vehicle under the control of the driver's commands,
|
||||||
|
* but leave it in a cancellable auto-drive.
|
||||||
|
* Stop all movement entirely.
|
||||||
|
* @param vehicle the vehicle
|
||||||
|
*/
|
||||||
|
def ConditionalDriverVehicleControl(vehicle: Vehicle): Unit = {
|
||||||
|
if (serverVehicleControlVelocity.nonEmpty && !serverVehicleControlVelocity.contains(0)) {
|
||||||
|
TotalDriverVehicleControl(vehicle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
|
||||||
|
serverVehicleControlVelocity = None
|
||||||
|
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false, lock_wheel=false, reverse=false, unk4=false, 0, 0, 0, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
def CanNotChangeDeployment(
|
||||||
|
obj: PlanetSideServerObject with Deployment,
|
||||||
|
state: DriveState.Value,
|
||||||
|
reason: String
|
||||||
|
): Unit = {
|
||||||
|
val mobileShift: String = 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)
|
||||||
|
)
|
||||||
|
"; enforcing Mobile deployment state"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,7 @@ object Default {
|
||||||
final def Cancellable: Cancellable = cancellable
|
final def Cancellable: Cancellable = cancellable
|
||||||
|
|
||||||
//actor
|
//actor
|
||||||
import akka.actor.{Actor => AkkaActor, ActorRef, ActorSystem, DeadLetter, Props}
|
import akka.actor.{Actor => AkkaActor, ActorRef, ActorSystem, DeadLetter, Props, typed => Typed}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An actor designed to wrap around `deadLetters` and redirect all normal messages to it.
|
* An actor designed to wrap around `deadLetters` and redirect all normal messages to it.
|
||||||
|
|
@ -47,4 +47,15 @@ object Default {
|
||||||
}
|
}
|
||||||
|
|
||||||
final def Actor: ActorRef = defaultRef
|
final def Actor: ActorRef = defaultRef
|
||||||
|
|
||||||
|
object typed {
|
||||||
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
|
private val defaultTypedRef: Typed.ActorRef[Any] = defaultRef.toTyped[Any]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy of the default actor
|
||||||
|
* but promoted into a typed actor that accepts any kind of message.
|
||||||
|
*/
|
||||||
|
final def Actor: Typed.ActorRef[Any] = defaultTypedRef
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,23 @@ class OrbitalShuttlePadControl(pad: OrbitalShuttlePad) extends Actor {
|
||||||
newShuttle.Position = position + Vector3(0, -8.25f, 0).Rz(pad.Orientation.z) //magic offset number
|
newShuttle.Position = position + Vector3(0, -8.25f, 0).Rz(pad.Orientation.z) //magic offset number
|
||||||
newShuttle.Orientation = pad.Orientation
|
newShuttle.Orientation = pad.Orientation
|
||||||
newShuttle.Faction = pad.Faction
|
newShuttle.Faction = pad.Faction
|
||||||
TaskWorkflow.execute(OrbitalShuttlePadControl.registerShuttle(zone, newShuttle, self))
|
shuttleRegistration(zone, newShuttle, self)
|
||||||
context.become(shuttleTime)
|
context.become(shuttleTime)
|
||||||
|
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the new shuttle fails to register the nth time, try again.
|
||||||
|
* Don't take "no" for an answer.
|
||||||
|
*/
|
||||||
|
def shuttleRegistration(zone: Zone, newShuttle: OrbitalShuttle, to: ActorRef): Future[Any] = {
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
TaskWorkflow.execute(OrbitalShuttlePadControl.registerShuttle(zone, newShuttle, to)).recover({
|
||||||
|
case _: Exception =>
|
||||||
|
if (!newShuttle.HasGUID) shuttleRegistration(zone, newShuttle, to)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object OrbitalShuttlePadControl {
|
object OrbitalShuttlePadControl {
|
||||||
|
|
@ -182,7 +194,7 @@ object OrbitalShuttlePadControl {
|
||||||
p.Name,
|
p.Name,
|
||||||
AvatarAction.SendResponse(
|
AvatarAction.SendResponse(
|
||||||
Service.defaultPlayerGUID,
|
Service.defaultPlayerGUID,
|
||||||
ChatMsg(ChatMessageType.UNK_225, false, "", "@DoorWillOpenWhenShuttleReturns", None)
|
ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "@DoorWillOpenWhenShuttleReturns", None)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
p.Name
|
p.Name
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ class BfrControl(vehicle: Vehicle)
|
||||||
def chargeShieldsOnly(amount: Int): Unit = {
|
def chargeShieldsOnly(amount: Int): Unit = {
|
||||||
val definition = vehicle.Definition
|
val definition = vehicle.Definition
|
||||||
val before = vehicle.Shields
|
val before = vehicle.Shields
|
||||||
if (canChargeShields()) {
|
if (canChargeShields) {
|
||||||
val chargeAmount = math.max(1, ((if (vehicle.DeploymentState == DriveState.Kneeling && vehicle.Seats(0).occupant.nonEmpty) {
|
val chargeAmount = math.max(1, ((if (vehicle.DeploymentState == DriveState.Kneeling && vehicle.Seats(0).occupant.nonEmpty) {
|
||||||
definition.ShieldAutoRechargeSpecial
|
definition.ShieldAutoRechargeSpecial
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ class VehicleControl(vehicle: Vehicle)
|
||||||
damageableVehiclePostStop()
|
damageableVehiclePostStop()
|
||||||
decaying = false
|
decaying = false
|
||||||
decayTimer.cancel()
|
decayTimer.cancel()
|
||||||
|
passengerRadiationCloudTimer.cancel()
|
||||||
vehicle.Utilities.values.foreach { util =>
|
vehicle.Utilities.values.foreach { util =>
|
||||||
context.stop(util().Actor)
|
context.stop(util().Actor)
|
||||||
util().Actor = Default.Actor
|
util().Actor = Default.Actor
|
||||||
|
|
@ -557,14 +558,14 @@ class VehicleControl(vehicle: Vehicle)
|
||||||
}
|
}
|
||||||
|
|
||||||
//make certain vehicles don't charge shields too quickly
|
//make certain vehicles don't charge shields too quickly
|
||||||
def canChargeShields(): Boolean = {
|
def canChargeShields: Boolean = {
|
||||||
val func: VitalsActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition)
|
val func: VitalsActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition)
|
||||||
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
|
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
|
||||||
!vehicle.History.exists(func)
|
!vehicle.History.exists(func)
|
||||||
}
|
}
|
||||||
|
|
||||||
def chargeShields(amount: Int): Unit = {
|
def chargeShields(amount: Int): Unit = {
|
||||||
if (canChargeShields()) {
|
if (canChargeShields) {
|
||||||
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
|
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
|
||||||
vehicle.Shields = vehicle.Shields + amount
|
vehicle.Shields = vehicle.Shields + amount
|
||||||
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
|
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.objects.avatar.Certification
|
import net.psforever.objects.avatar.Certification
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
import net.psforever.types.PlanetSideGUID
|
|
||||||
import scodec.Codec
|
import scodec.Codec
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
import shapeless.{::, HNil}
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,13 @@ class LocalService(zone: Zone) extends Actor {
|
||||||
//response from HackClearActor
|
//response from HackClearActor
|
||||||
case HackClearActor.SendHackMessageHackCleared(target_guid, _, unk1, unk2) =>
|
case HackClearActor.SendHackMessageHackCleared(target_guid, _, unk1, unk2) =>
|
||||||
log.info(s"Clearing hack for $target_guid")
|
log.info(s"Clearing hack for $target_guid")
|
||||||
|
LocalEvents.publish(
|
||||||
|
LocalServiceResponse(
|
||||||
|
s"/${zone.id}/Local",
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
//message from ProximityTerminalControl
|
//message from ProximityTerminalControl
|
||||||
case Terminal.StartProximityEffect(terminal) =>
|
case Terminal.StartProximityEffect(terminal) =>
|
||||||
|
|
@ -326,9 +333,7 @@ class LocalService(zone: Zone) extends Actor {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Forward all CaptureFlagManager messages
|
// Forward all CaptureFlagManager messages
|
||||||
case msg @ (CaptureFlagManager.SpawnCaptureFlag(_, _, _) | CaptureFlagManager.PickupFlag(_, _) |
|
case msg: CaptureFlagManager.Command =>
|
||||||
CaptureFlagManager.DropFlag(_) | CaptureFlagManager.Captured(_) | CaptureFlagManager.Lost(_, _) |
|
|
||||||
CaptureFlagManager) =>
|
|
||||||
captureFlagManager.forward(msg)
|
captureFlagManager.forward(msg)
|
||||||
|
|
||||||
case msg =>
|
case msg =>
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,13 @@ class CaptureFlagManager(zone: Zone) extends Actor{
|
||||||
}
|
}
|
||||||
|
|
||||||
object CaptureFlagManager {
|
object CaptureFlagManager {
|
||||||
final case class SpawnCaptureFlag(capture_terminal: CaptureTerminal, target: Building, hackingFaction: PlanetSideEmpire.Value)
|
sealed trait Command
|
||||||
final case class PickupFlag(flag: CaptureFlag, player: Player)
|
|
||||||
final case class DropFlag(flag: CaptureFlag)
|
final case class SpawnCaptureFlag(capture_terminal: CaptureTerminal, target: Building, hackingFaction: PlanetSideEmpire.Value) extends Command
|
||||||
final case class Captured(flag: CaptureFlag)
|
final case class PickupFlag(flag: CaptureFlag, player: Player) extends Command
|
||||||
final case class Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum)
|
final case class DropFlag(flag: CaptureFlag) extends Command
|
||||||
|
final case class Captured(flag: CaptureFlag) extends Command
|
||||||
|
final case class Lost(flag: CaptureFlag, reason: CaptureFlagLostReasonEnum) extends Command
|
||||||
final case class MapUpdate()
|
final case class MapUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,56 +14,46 @@ class PropertyOverrideManager extends Actor {
|
||||||
private var gamePropertyScopes: List[PropertyOverrideMessage.GamePropertyScope] = List()
|
private var gamePropertyScopes: List[PropertyOverrideMessage.GamePropertyScope] = List()
|
||||||
lazy private val zoneIds: Iterable[Int] = Zones.zones.map(_.Number)
|
lazy private val zoneIds: Iterable[Int] = Zones.zones.map(_.Number)
|
||||||
|
|
||||||
override def preStart() = {
|
override def preStart(): Unit = {
|
||||||
LoadOverridesFromFile(zoneId = 0) // Global overrides
|
LoadOverridesFromFile(zoneId = 0) // Global overrides
|
||||||
for (zoneId <- zoneIds) {
|
for (zoneId <- zoneIds) {
|
||||||
LoadOverridesFromFile(zoneId)
|
LoadOverridesFromFile(zoneId)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessGamePropertyScopes()
|
ProcessGamePropertyScopes()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def receive: Receive = {
|
override def receive: Receive = {
|
||||||
case PropertyOverrideManager.GetOverridesMessage => {
|
case PropertyOverrideManager.GetOverridesMessage =>
|
||||||
sender() ! gamePropertyScopes
|
sender() ! gamePropertyScopes
|
||||||
}
|
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
||||||
private def LoadOverridesFromFile(zoneId: Int): Unit = {
|
private def LoadOverridesFromFile(zoneId: Int): Unit = {
|
||||||
val zoneOverrides = LoadFile(s"overrides/game_objects$zoneId.adb.lst")
|
val zoneOverrides = LoadFile(s"overrides/game_objects$zoneId.adb.lst")
|
||||||
|
|
||||||
if (zoneOverrides == null) {
|
if (zoneOverrides == null) {
|
||||||
log.debug(s"PropertyOverride: no overrides found for zone $zoneId using filename game_objects$zoneId.adb.lst")
|
log.debug(s"PropertyOverride: no overrides found for zone $zoneId using filename game_objects$zoneId.adb.lst")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val grouped = zoneOverrides.groupBy(_._1).view.mapValues(_.map(x => (x._2, x._3)).toList).toMap
|
val grouped = zoneOverrides.groupBy(_._1).view.mapValues(_.map(x => (x._2, x._3)).toList).toMap
|
||||||
|
|
||||||
log.debug(s"PropertyOverride: loaded property overrides for zone $zoneId: ${grouped.toString}")
|
log.debug(s"PropertyOverride: loaded property overrides for zone $zoneId: ${grouped.toString}")
|
||||||
overrides += (zoneId -> grouped)
|
overrides += (zoneId -> grouped)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def ProcessGamePropertyScopes(): Unit = {
|
private def ProcessGamePropertyScopes(): Unit = {
|
||||||
val scopesBuffer: ListBuffer[GamePropertyScope] = ListBuffer()
|
val scopesBuffer: ListBuffer[GamePropertyScope] = ListBuffer()
|
||||||
|
|
||||||
for (over <- overrides) {
|
for (over <- overrides) {
|
||||||
val zoneId = over._1
|
val zoneId = over._1
|
||||||
val overrideMap = over._2
|
val overrideMap = over._2
|
||||||
|
|
||||||
val gamePropertyTargets: ListBuffer[PropertyOverrideMessage.GamePropertyTarget] = ListBuffer()
|
val gamePropertyTargets: ListBuffer[PropertyOverrideMessage.GamePropertyTarget] = ListBuffer()
|
||||||
|
|
||||||
for (propOverride <- overrideMap) {
|
for (propOverride <- overrideMap) {
|
||||||
val objectId = ObjectClass.ByName(propOverride._1)
|
val objectId = ObjectClass.ByName(propOverride._1)
|
||||||
val props = GamePropertyTarget(objectId, propOverride._2)
|
val props = GamePropertyTarget(objectId, propOverride._2)
|
||||||
gamePropertyTargets += props
|
gamePropertyTargets += props
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = GamePropertyScope(zoneId, gamePropertyTargets.toList)
|
val scope = GamePropertyScope(zoneId, gamePropertyTargets.toList)
|
||||||
|
|
||||||
scopesBuffer += scope
|
scopesBuffer += scope
|
||||||
}
|
}
|
||||||
|
|
||||||
gamePropertyScopes = scopesBuffer.toList
|
gamePropertyScopes = scopesBuffer.toList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,33 +62,26 @@ class PropertyOverrideManager extends Actor {
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val content = scala.io.Source.fromInputStream(stream).getLines().filter(x => x.startsWith("add_property"))
|
val content = scala.io.Source.fromInputStream(stream).getLines().filter(x => x.startsWith("add_property"))
|
||||||
var data: ListBuffer[(String, String, String)] = ListBuffer()
|
val data: ListBuffer[(String, String, String)] = ListBuffer()
|
||||||
|
|
||||||
for (line <- content) {
|
for (line <- content) {
|
||||||
val splitLine = line.split(" ")
|
val splitLine = line.split(" ")
|
||||||
if (splitLine.length >= 3) {
|
if (splitLine.length >= 3) {
|
||||||
val objectName = splitLine(1)
|
val objectName = splitLine(1)
|
||||||
val property = splitLine(2)
|
val property = splitLine(2)
|
||||||
|
|
||||||
var propertyValue = ""
|
var propertyValue = ""
|
||||||
for (i <- 3 to splitLine.length - 1) {
|
for (i <- 3 until splitLine.length) {
|
||||||
propertyValue += splitLine(i) + " "
|
propertyValue += splitLine(i) + " "
|
||||||
}
|
}
|
||||||
propertyValue = propertyValue.trim
|
propertyValue = propertyValue.trim
|
||||||
|
|
||||||
data += ((objectName, property, propertyValue))
|
data += ((objectName, property, propertyValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object PropertyOverrideManager {
|
object PropertyOverrideManager {
|
||||||
final case class GetOverridesMessage()
|
final case object GetOverridesMessage
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue