Misc Fixes (#1076)

* new paradigm for character creation detection of old characters by name; vehicle channel when seated in vehicle (ant); second wind activates as long as non-fatal damage n>=25; coordinated sequence of deployables whose UI is being updated

* the max timer will assert itself through death and respawn

* in theory, the tests are fixed; that may change from execution to execution, as is usual

* adjusted how the mechanized exo-suit timer asserts itself when in conjunction with prior exo-suit purchase orders

* players in seats have their mounted information shortened in a more straightforward, less fault-prone way; stamina recharge command shortened

* fixed vehicles not loading when player has no GUID; deactivated squad features (may cause trouble for the Router, but we'll manage); removed lingering, unnecessary radiation tick

* even if the player seems to be standing completely still, send an update packet once in a while (1500ms)

* removing an active router will always clean up an active router telepad with which it is paired

* better timing for refresh of the character select screen; potential to stop moving vehicles from anothers's perspectives

* block mounting while vehicle in motion, or in control; if ejected early, end control early

* block mounting while vehicle in motion, or in control (2)
This commit is contained in:
Fate-JH 2023-05-15 22:24:35 -04:00 committed by GitHub
parent 66f45edcd3
commit 70c4393e9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 593 additions and 392 deletions

View file

@ -90,6 +90,9 @@ game {
# Purchases timers for the mechanized assault exo-suits all update at the same time when any of them would update
shared-max-cooldown = no
# Purchases timers for the battleframe robotics vehicles all update at the same time when either of them would update
shared-bfr-cooldown = yes
# HART system, shuttles and facilities
hart {
# How long the shuttle is not boarding passengers (going through the motions)
@ -208,7 +211,7 @@ game {
# Use the index of that sample distance from this sequence in the sequence `delays` below.
ranges = [150, 300, 400]
# The absolute time delay before a successful packet must be dispatched regardless of distance. (s)
delay-max = 1000
delay-max = 1500
# The time delays for each distance range before a successful packet must be dispatched. [s]
# The index for an entry in this sequence is expected to be discovered using the `ranges` sequence above.
# Delays between packets may not be as precise as desired

View file

@ -1,24 +1,17 @@
// Copyright (c) 2019 PSForever
package net.psforever.actors.session
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.avatar.{ProgressDecoration, SpecialCarry}
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill}
import net.psforever.objects.sourcing.SourceWithHealthEntry
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, SpawningActivity, Vitality}
import net.psforever.packet.game.objectcreate.BasicCharacterData
import net.psforever.types.ExperienceType
import java.util.concurrent.atomic.AtomicInteger
import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
import scala.concurrent.duration._
//
import net.psforever.objects._
import net.psforever.objects.avatar.{
Avatar,
BattleRank,
@ -29,25 +22,31 @@ import net.psforever.objects.avatar.{
Implant,
MemberLists,
PlayerControl,
Shortcut => AvatarShortcut
ProgressDecoration,
Shortcut => AvatarShortcut,
SpecialCarry
}
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill}
import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.inventory.Container
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects._
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.HealFromImplant
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity, Vitality}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.persistence
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{
CharacterSex,
CharacterVoice,
Cosmetic,
ExoSuitType,
ExperienceType,
ImplantType,
LoadoutType,
MemberAction,
@ -57,11 +56,7 @@ import net.psforever.types.{
TransactionType
}
import net.psforever.util.Database._
import net.psforever.persistence
import net.psforever.util.{Config, Database, DefinitionUtil}
import net.psforever.services.Service
//import org.log4s.Logger
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
object AvatarActor {
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
@ -400,28 +395,33 @@ object AvatarActor {
def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = {
val (definition, name) = pair
if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) {
//all mechanized exo-suits
val faction = name.take(2)
(if (faction.equals("nc")) {
Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow)
} else if (faction.equals("vs")) {
Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire)
} else {
Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster)
}).zip(
Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft")
)
} else {
Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow)
} else if (faction.equals("vs")) {
Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire)
} else {
Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster)
}).map { tdef => (tdef, tdef.Descriptor) }
} else if ((name.matches(".+_gunner") || name.matches(".+_flight")) && Config.app.game.sharedBfrCooldown) {
//all battleframe robotics vehicles
definition match {
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameFlightVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val gunner = bframe + "_gunner"
Seq((DefinitionUtil.fromString(gunner), gunner), (vdef, name))
Seq((DefinitionUtil.fromString(gunner), gunner), pair)
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameGunnerVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val flight = bframe + "_flight"
Seq((vdef, name), (DefinitionUtil.fromString(flight), flight))
Seq(pair, (DefinitionUtil.fromString(flight), flight))
case _ =>
Seq.empty
}
} else {
definition match {
case tdef: ToolDefinition if GlobalDefinitions.isMaxArms(tdef) =>
Seq((tdef, tdef.Descriptor))
case _ =>
Seq(pair)
}
@ -913,43 +913,8 @@ class AvatarActor(
AvatarActor.displayLookingForSquad(session.get, if (lfs) 1 else 0)
Behaviors.same
case CreateAvatar(name, head, voice, gender, empire) =>
import ctx._
ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete {
case Success(characters) =>
characters.headOption match {
case None =>
val result = for {
_ <- ctx.run(
query[persistence.Avatar]
.insert(
_.name -> lift(name),
_.accountId -> lift(account.id),
_.factionId -> lift(empire.id),
_.headId -> lift(head),
_.voiceId -> lift(voice.id),
_.genderId -> lift(gender.value),
_.bep -> lift(Config.app.game.newAvatar.br.experience),
_.cep -> lift(Config.app.game.newAvatar.cr.experience)
)
)
} yield ()
result.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: created character $name for account ${account.name}")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
sendAvatars(account)
case Failure(e) => log.error(e)("db failure")
}
case Some(_) =>
// send "char already exists"
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(1))
}
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(3))
sendAvatars(account)
}
case CreateAvatar(name, head, voice, sex, empire) =>
createAvatar(account, name, sex, empire, head, voice)
Behaviors.same
case DeleteAvatar(id) =>
@ -1482,7 +1447,8 @@ class AvatarActor(
Behaviors.same
case UpdatePurchaseTime(definition, time) =>
var newTimes = avatar.cooldowns.purchase
var theTimes = avatar.cooldowns.purchase
var updateTheTimes: Boolean = false
AvatarActor
.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition))
.foreach {
@ -1490,7 +1456,8 @@ class AvatarActor(
Avatar.purchaseCooldowns.get(item) match {
case Some(cooldown) =>
//only send for items with cooldowns
newTimes = newTimes.updated(name, time)
updateTheTimes = true
theTimes = theTimes.updated(name, time)
updatePurchaseTimer(
name,
cooldown.toSeconds,
@ -1502,7 +1469,9 @@ class AvatarActor(
case _ => ;
}
}
avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = newTimes)))
if (updateTheTimes) {
avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = theTimes)))
}
Behaviors.same
case UpdateUseTime(definition, time) =>
@ -1632,11 +1601,7 @@ class AvatarActor(
Behaviors.same
case RestoreStamina(stamina) =>
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStamina(stamina, sess)
case _ => ;
}
tryRestoreStaminaForSession(stamina).collect { actuallyRestoreStamina(stamina, _) }
Behaviors.same
case RestoreStaminaPeriodically(stamina) =>
@ -1897,11 +1862,7 @@ class AvatarActor(
)
)
// if we need to start stamina regeneration
tryRestoreStaminaForSession(stamina = 1) match {
case Some(_) =>
defaultStaminaRegen(initialDelay = 0.5f seconds)
case _ => ;
}
tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
replyTo ! AvatarLoginResponse(avatar)
case Failure(e) =>
log.error(e)("db failure")
@ -1991,11 +1952,7 @@ class AvatarActor(
}
def restoreStaminaPeriodically(stamina: Int): Unit = {
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStaminaIfStationary(stamina, sess)
case _ => ;
}
tryRestoreStaminaForSession(stamina).collect { actuallyRestoreStaminaIfStationary(stamina, _) }
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
}
@ -2633,27 +2590,40 @@ class AvatarActor(
def refreshPurchaseTimes(keys: Set[String]): Unit = {
var keysToDrop: Seq[String] = Nil
val faction = avatar.faction
keys.foreach { key =>
avatar.cooldowns.purchase.find { case (name, _) => name.equals(key) } match {
case Some((name, purchaseTime)) =>
avatar
.cooldowns
.purchase
.find { case (name, _) => name.equals(key) }
.flatMap { case (name, purchaseTime) =>
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
Avatar.purchaseCooldowns.find(_._1.Name.equals(name)) match {
case Some((obj, cooldown)) if cooldown.toSeconds - secondsSincePurchase > 0 =>
val (_, name) = AvatarActor.resolvePurchaseTimeName(avatar.faction, obj)
updatePurchaseTimer(
name,
cooldown.toSeconds - secondsSincePurchase,
obj match {
case _: KitDefinition => false
case _ => true
}
)
case _ =>
keysToDrop = keysToDrop :+ key //key has timed-out
Avatar
.purchaseCooldowns
.find(_._1.Descriptor.equals(name))
.collect {
case (obj, cooldown) =>
(obj, cooldown.toSeconds - secondsSincePurchase)
}
.orElse {
keysToDrop = keysToDrop :+ key //key indicates cooldown, but no cooldown delay
None
}
case _ => ;
}
}
.collect {
case (obj, remainingTime) if remainingTime > 0 =>
val (_, resolvedName) = AvatarActor.resolvePurchaseTimeName(faction, obj)
updatePurchaseTimer(
resolvedName,
remainingTime,
obj match {
case _: KitDefinition => false
case _ => true
}
)
case (_, _) =>
keysToDrop = keysToDrop :+ key //key has timed-out
}
}
if (keysToDrop.nonEmpty) {
val cdown = avatar.cooldowns
@ -2999,4 +2969,131 @@ class AvatarActor(
def updateToolDischarge(stats: EquipmentStat): Unit = {
avatar.scorecard.rate(stats)
}
def createAvatar(
account: Account,
name: String,
sex: CharacterSex,
empire: PlanetSideEmpire.Value,
head: Int,
voice: CharacterVoice.Value
): Unit = {
import ctx._
ctx.run(
query[persistence.Avatar]
.filter(_.name ilike lift(name))
.map(a => (a.accountId, a.deleted, a.created))
)
.onComplete {
case Success(foundCharacters) if foundCharacters.size == 1 =>
testFoundCharacters(
account,
name,
foundCharacters
.collect { case (a, b, c) => (a, b, c.toDate.getTime) }
)
case Success(foundCharacters) if foundCharacters.nonEmpty =>
testFoundCharacters(
account,
name,
foundCharacters
.map { case (a, b, created) => (a, b, created.toDate.getTime) }
.sortBy { case (_, _, created) => created }
.toList
)
case Success(_) =>
//create new character
actuallyCreateNewCharacter(account.id, account.name, name, sex, empire, head, voice)
.onComplete(_ => sendAvatars(account))
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 3))
sendAvatars(account)
}
}
private def testFoundCharacters(
account: Account,
name: String,
foundCharacters: IterableOnce[(Int, Boolean, Long)]): Unit = {
val foundCharactersIterator = foundCharacters.iterator
if (foundCharactersIterator.exists { case (_, deleted, _ ) => !deleted } ||
foundCharactersIterator.exists { case (accountId, _, _) => accountId != account.id }) {
//send "char already exists"
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 1))
} else {
//reactivate character
reactivateCharacter(account.id, account.name, name)
.onComplete(_ => sendAvatars(account))
}
}
private def actuallyCreateNewCharacter(
accountId: Int,
accountName: String,
name: String,
sex: CharacterSex,
empire: PlanetSideEmpire.Value,
head: Int,
voice: CharacterVoice.Value,
bep: Long = Config.app.game.newAvatar.br.experience,
cep: Long = Config.app.game.newAvatar.cr.experience
): Future[Boolean] = {
val output: Promise[Boolean] = Promise[Boolean]()
import ctx._
val result = for {
_ <- ctx.run(
query[persistence.Avatar]
.insert(
_.name -> lift(name),
_.accountId -> lift(accountId),
_.factionId -> lift(empire.id),
_.headId -> lift(head),
_.voiceId -> lift(voice.id),
_.genderId -> lift(sex.value),
_.bep -> lift(bep),
_.cep -> lift(cep)
)
)
} yield ()
result.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: created character $name for account $accountName")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
output.success(true)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 3))
output.success(false)
}
output.future
}
private def reactivateCharacter(
accountId: Int,
accountName: String,
characterName: String
): Future[Boolean] = {
val output: Promise[Boolean] = Promise[Boolean]()
import ctx._
val result = for {
out <- ctx.run(
query[persistence.Avatar]
.filter(a => a.accountId == lift(accountId))
.filter(a => a.name ilike lift(characterName))
.update(_.deleted -> lift(false))
)
} yield out
result.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: character belonging to $accountName with name $characterName reactivated")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
output.success(true)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 3))
output.success(false)
}
output.future
}
}

View file

@ -104,10 +104,10 @@ class SessionAvatarHandlers(
} //ms
if (!wasVisible ||
!previouslyInDrawableRange ||
durationSince > drawConfig.delayMax ||
(!lastMsg.contains(pstateToSave) &&
(canSeeReallyFar ||
currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
durationSince > drawConfig.delayMax ||
sessionData.canSeeReallyFar ||
durationSince > targetDelay
)
@ -425,8 +425,7 @@ class SessionAvatarHandlers(
sessionData.zoning.zoningStatus = Zoning.Status.None
sessionData.zoning.spawn.deadState = DeadState.Dead
continent.GUID(mount).collect { case obj: Vehicle =>
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
sessionData.vehicles.serverVehicleControlVelocity = None
sessionData.vehicles.DriverVehicleControl(obj)
sessionData.unaccessContainer(obj)
}
sessionData.playerActionsToCancel()

View file

@ -16,7 +16,7 @@ import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleM
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}
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
class SessionMountHandlers(
val sessionData: SessionData,
@ -56,6 +56,7 @@ class SessionMountHandlers(
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionData.accessContainer(obj)
MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
@ -237,7 +238,7 @@ class SessionMountHandlers(
}")
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
sessionData.unaccessContainer(obj)
DismountAction(tplayer, obj, seatNum)
DismountVehicleAction(tplayer, obj, seatNum)
case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
continent.VehicleEvents ! VehicleServiceMessage(
@ -288,6 +289,43 @@ class SessionMountHandlers(
)
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking
*/
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = {
DismountAction(tplayer, obj, seatNum)
//until vehicles maintain synchronized momentum without a driver
obj match {
case v: Vehicle
if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
sessionData.vehicles.serverVehicleControlVelocity.collect { _ =>
sessionData.vehicles.ServerVehicleOverrideStop(v)
}
v.Velocity = Vector3.Zero
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
tplayer.GUID,
v.GUID,
unk1 = 0,
v.Position,
v.Orientation,
vel = None,
v.Flying,
unk3 = 0,
unk4 = 0,
wheel_direction = 15,
unk5 = false,
unk6 = v.Cloaked
)
)
case _ => ()
}
}
/**
* Common activities/procedure when a player dismounts a valid mountable object.
* @param tplayer the player
@ -305,7 +343,7 @@ class SessionMountHandlers(
sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.DismountVehicle(playerGuid, bailType, unk2=false)
VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
)
}
}

View file

@ -55,17 +55,17 @@ class SessionSquadHandlers(
/* packet */
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
val SquadDefinitionActionMessage(u1, u2, action) = pkt
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
// 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)
)
// 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 = {

View file

@ -34,7 +34,7 @@ class SessionVehicleHandlers(
val resolvedPlayerGuid = if (player.HasGUID) {
player.GUID
} else {
Service.defaultPlayerGUID
PlanetSideGUID(-1)
}
val isNotSameTarget = resolvedPlayerGuid != guid
reply match {
@ -202,17 +202,19 @@ class SessionVehicleHandlers(
val strafe = 1 + Vehicles.CargoOrientation(vehicle)
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,
lock_vthrust=0,
strafe,
reverseSpeed,
unk8=Some(0)
))
sessionData.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=true,
lock_wheel=true,
reverse=true,
unk4=false,
lock_vthrust=0,
strafe,
reverseSpeed,
unk8=Some(0)
)
)
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(
delay milliseconds,
@ -220,25 +222,27 @@ class SessionVehicleHandlers(
VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay))
)
case VehicleResponse.KickCargo(_, _, _)
case VehicleResponse.KickCargo(cargo, _, _)
if player.VehicleSeated.nonEmpty && sessionData.zoning.spawn.deadState == DeadState.Alive =>
sessionData.vehicles.serverVehicleControlVelocity = None
sendResponse(ServerVehicleOverrideMsg(
lock_accelerator=false,
lock_wheel=false,
reverse=false,
unk4=false,
lock_vthrust=0,
lock_strafe=0,
movement_speed=0,
unk8=None
))
sessionData.vehicles.TotalDriverVehicleControl(cargo)
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
if player.VisibleSlots.contains(player.DrawnSlot) =>
val vehicle_guid = vehicle.GUID
sessionData.playerActionsToCancel()
sessionData.vehicles.serverVehicleControlVelocity = Some(0)
sessionData.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=false,
lock_wheel=false,
reverse=false,
unk4=false,
lock_vthrust=0,
lock_strafe=0,
movement_speed=0,
unk8=None
)
)
sessionData.terminals.CancelAllProximityUnits()
player.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1=true))
@ -253,33 +257,51 @@ class SessionVehicleHandlers(
}
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
val vehicle_guid = vehicle.GUID
val vehicleGuid = vehicle.GUID
val playerGuid = player.GUID
sessionData.playerActionsToCancel()
sessionData.vehicles.serverVehicleControlVelocity = Some(0)
sessionData.terminals.CancelAllProximityUnits()
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
sendResponse(PlanetsideAttributeMessage(vehicleGuid, attribute_type=22, attribute_value=1L)) //mount points off
sendResponse(PlanetsideAttributeMessage(playerGuid, attribute_type=21, vehicleGuid)) //ownership
vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
}
case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
val vehicle_guid = vehicle.GUID
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=0L)) //mount points on
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
sessionData.vehicles.ServerVehicleLock(vehicle)
sessionData.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=true,
lock_wheel=true,
reverse=false,
unk4=false,
lock_vthrust=0,
lock_strafe=0,
movement_speed=0,
unk8=Some(0)
)
)
sessionData.vehicles.serverVehicleControlVelocity = Some(-1)
case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
val vdef = vehicle.Definition
sessionData.vehicles.ServerVehicleOverride(
sessionData.vehicles.ServerVehicleOverrideWithPacket(
vehicle,
vdef.AutoPilotSpeed1,
flight= if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 }
ServerVehicleOverrideMsg(
lock_accelerator=true,
lock_wheel=true,
reverse=false,
unk4=false,
lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
lock_strafe=0,
movement_speed=vdef.AutoPilotSpeed1,
unk8=Some(0)
)
)
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
session = session.copy(avatar = avatar.copy(vehicle = Some(vehicle.GUID)))
sessionData.vehicles.DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
sessionData.vehicles.ServerVehicleOverrideStop(vehicle)
case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
sendResponse(ChatMsg(

View file

@ -268,22 +268,23 @@ class VehicleOperations(
def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
sessionData.validObject(mountable_guid, decorator = "MountVehicle") match {
case Some(obj: Mountable) =>
sessionData.validObject(mountable_guid, decorator = "MountVehicle").collect {
case obj: Mountable =>
obj.Actor ! Mountable.TryMount(player, entry_point)
case Some(_) =>
case _: Mountable =>
log.warn(
s"DismountVehicleMsg: ${player.Name} can not mount while server has asserted control; please wait"
)
case _ =>
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
val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
//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 {
@ -295,16 +296,9 @@ class VehicleOperations(
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))
dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
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) =>
@ -325,15 +319,14 @@ class VehicleOperations(
}
case None =>
dismountWarning(
s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}"
)
dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
}
case _ =>
dismountWarning(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}")
dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
}
} else {
//kicking someone else out of a mount; need to own that mount/mountable
val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
player.avatar.vehicle match {
case Some(obj_guid) =>
(
@ -353,23 +346,47 @@ class VehicleOperations(
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"
)
dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
}
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 (None, _) =>
dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
case (_, None) =>
dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
case _ =>
log.warn(s"DismountVehicleMsg: object is either not a Mountable or not a Player")
dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
}
case None =>
log.warn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle")
dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
}
}
}
private def dismountWarning(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.warn(note)
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
private def dismountError(
bailAs: BailType.Value,
kickedByDriver: Boolean
)
(
note: String,
player: Player
): Unit = {
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
player.VehicleSeated = None
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
}
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
@ -514,87 +531,27 @@ class VehicleOperations(
}
/**
* 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
* Place the current vehicle under the control of the driver's commands,
* but leave it in a cancellable auto-drive.
* @param vehicle the vehicle
*/
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)))
def ServerVehicleOverrideStop(vehicle: Vehicle): Unit = {
val vehicleGuid = vehicle.GUID
session = session.copy(avatar = avatar.copy(vehicle = Some(vehicleGuid)))
sessionData.vehicles.DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
sendResponse(PlanetsideAttributeMessage(vehicleGuid, attribute_type=22, attribute_value=0L)) //mount points on
}
/**
* 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
* @param vehicle vehicle to be controlled;
* the client's player who is receiving this packet should be mounted as its driver, but this is not explicitly tested
* @param pkt packet to instigate server control
*/
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)))
def ServerVehicleOverrideWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
serverVehicleControlVelocity = Some(pkt.movement_speed)
vehicle.DeploymentState = DriveState.AutoPilot
sendResponse(pkt)
}
/**
@ -605,9 +562,20 @@ class VehicleOperations(
* @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))
if (vehicle.DeploymentState == DriveState.AutoPilot) {
TotalDriverVehicleControlWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=false,
lock_wheel=false,
reverse=false,
unk4=true,
lock_vthrust=flight,
lock_strafe=0,
movement_speed=speed,
unk8=None
)
)
}
}
@ -618,15 +586,45 @@ class VehicleOperations(
* @param vehicle the vehicle
*/
def ConditionalDriverVehicleControl(vehicle: Vehicle): Unit = {
if (serverVehicleControlVelocity.nonEmpty && !serverVehicleControlVelocity.contains(0)) {
if (vehicle.DeploymentState == DriveState.AutoPilot) {
TotalDriverVehicleControl(vehicle)
}
}
//noinspection ScalaUnusedSymbol
/**
* 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 TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
TotalDriverVehicleControlWithPacket(
vehicle,
ServerVehicleOverrideMsg(
lock_accelerator=false,
lock_wheel=false,
reverse=false,
unk4=false,
lock_vthrust=0,
lock_strafe=0,
movement_speed=0,
unk8=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;
* the client's player who is receiving this packet should be mounted as its driver, but this is not explicitly tested
* @param pkt packet to instigate cancellable control
*/
def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
serverVehicleControlVelocity = None
sendResponse(ServerVehicleOverrideMsg(lock_accelerator=false, lock_wheel=false, reverse=false, unk4=false, 0, 0, 0, None))
vehicle.DeploymentState = DriveState.Mobile
sendResponse(pkt)
}
/**

View file

@ -82,19 +82,16 @@ object Deployables {
val zone = target.Zone
val events = zone.LocalEvents
val item = target.Definition.Item
target.OwnerName match {
case Some(owner) =>
zone.Players.find { p => owner.equals(p.name) } match {
case Some(p) =>
if (p.deployables.Remove(target)) {
events ! LocalServiceMessage(owner, LocalAction.DeployableUIFor(item))
}
case None => ;
}
target.OwnerName
.foreach { owner =>
zone.LivePlayers.find { p => owner.equals(p.Name) }
.orElse(zone.Corpses.find { c => owner.equals(c.Name) })
.foreach { p =>
p.Actor ! Player.LoseDeployable(target)
}
target.Owner = None
target.OwnerName = None
case None => ;
}
}
events ! LocalServiceMessage(
s"${target.Faction}",
LocalAction.DeployableMapIcon(
@ -114,6 +111,7 @@ object Deployables {
* @return all previously-owned deployables after they have been processed;
* boomers are listed before all other deployable types
*/
//noinspection ScalaUnusedSymbol
def Disown(zone: Zone, avatar: Avatar, replyTo: ActorRef): List[Deployable] = {
avatar.deployables
.Clear()

View file

@ -5411,6 +5411,7 @@ object GlobalDefinitions {
dynomite.Tile = InventoryTile.Tile22
trhev_dualcycler.Name = "trhev_dualcycler"
trhev_dualcycler.Descriptor = "trhev_antipersonnel"
trhev_dualcycler.Size = EquipmentSize.Max
trhev_dualcycler.AmmoTypes += dualcycler_ammo
trhev_dualcycler.ProjectileTypes += dualcycler_projectile
@ -5428,6 +5429,7 @@ object GlobalDefinitions {
trhev_dualcycler.FireModes(2).Magazine = 200
trhev_pounder.Name = "trhev_pounder"
trhev_pounder.Descriptor = "trhev_antivehicular"
trhev_pounder.Size = EquipmentSize.Max
trhev_pounder.AmmoTypes += pounder_ammo
trhev_pounder.ProjectileTypes += pounder_projectile
@ -5461,6 +5463,7 @@ object GlobalDefinitions {
trhev_pounder.FireModes(5).Magazine = 30
trhev_burster.Name = "trhev_burster"
trhev_burster.Descriptor = "trhev_antiaircraft"
trhev_burster.Size = EquipmentSize.Max
trhev_burster.AmmoTypes += burster_ammo
trhev_burster.ProjectileTypes += burster_projectile
@ -5470,6 +5473,7 @@ object GlobalDefinitions {
trhev_burster.FireModes.head.Magazine = 40
nchev_scattercannon.Name = "nchev_scattercannon"
nchev_scattercannon.Descriptor = "nchev_antipersonnel"
nchev_scattercannon.Size = EquipmentSize.Max
nchev_scattercannon.AmmoTypes += scattercannon_ammo
nchev_scattercannon.ProjectileTypes += scattercannon_projectile
@ -5490,6 +5494,7 @@ object GlobalDefinitions {
nchev_scattercannon.FireModes(2).Chamber = 10 //40 shells * 10 pellets = 400
nchev_falcon.Name = "nchev_falcon"
nchev_falcon.Descriptor = "nchev_antivehicular"
nchev_falcon.Size = EquipmentSize.Max
nchev_falcon.AmmoTypes += falcon_ammo
nchev_falcon.ProjectileTypes += falcon_projectile
@ -5499,6 +5504,7 @@ object GlobalDefinitions {
nchev_falcon.FireModes.head.Magazine = 20
nchev_sparrow.Name = "nchev_sparrow"
nchev_sparrow.Descriptor = "nchev_antiaircraft"
nchev_sparrow.Size = EquipmentSize.Max
nchev_sparrow.AmmoTypes += sparrow_ammo
nchev_sparrow.ProjectileTypes += sparrow_projectile
@ -5508,6 +5514,7 @@ object GlobalDefinitions {
nchev_sparrow.FireModes.head.Magazine = 12
vshev_quasar.Name = "vshev_quasar"
vshev_quasar.Descriptor = "vshev_antipersonnel"
vshev_quasar.Size = EquipmentSize.Max
vshev_quasar.AmmoTypes += quasar_ammo
vshev_quasar.ProjectileTypes += quasar_projectile
@ -5523,6 +5530,7 @@ object GlobalDefinitions {
vshev_quasar.FireModes(1).Magazine = 120
vshev_comet.Name = "vshev_comet"
vshev_comet.Descriptor = "vshev_antivehicular"
vshev_comet.Size = EquipmentSize.Max
vshev_comet.AmmoTypes += comet_ammo
vshev_comet.ProjectileTypes += comet_projectile
@ -5532,6 +5540,7 @@ object GlobalDefinitions {
vshev_comet.FireModes.head.Magazine = 10
vshev_starfire.Name = "vshev_starfire"
vshev_starfire.Descriptor = "vshev_antiaircraft"
vshev_starfire.Size = EquipmentSize.Max
vshev_starfire.AmmoTypes += starfire_ammo
vshev_starfire.ProjectileTypes += starfire_projectile

View file

@ -36,7 +36,7 @@ import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.collision.CollisionReason
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult}
import net.psforever.services.hart.ShuttleState
import net.psforever.packet.PlanetSideGamePacket
@ -368,8 +368,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
val fallbackSubtype = 0
val fallbackSuit = ExoSuitType.Standard
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
//sanitize exo-suit for change
@ -392,29 +390,39 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}) ++ dropHolsters ++ dropInventory
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
//imposed 5min delay on mechanized exo-suit switches
val (nextSuit, nextSubtype) =
if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
player.ResistArmMotion(PlayerControl.maxRestriction)
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) => false
case None =>
val (nextSuit, nextSubtype) = {
lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) {
//TODO will we ever need to check for the cooldown status of an original non-MAX exo-suit?
(originalSuit, originalSubtype)
} else {
(ExoSuitType.Standard, 0)
}
if (Players.CertificationToUseExoSuit(player, exosuit, subtype)) {
if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
val cooldown = player.avatar.purchaseCooldown(weapon)
if (originalSubtype == subtype) {
(exosuit, subtype) //same MAX subtype is free
} else if (cooldown.nonEmpty) {
fallbackSuit //different MAX subtype can not have cooldown
} else {
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
(exosuit, subtype) //switching for first time causes cooldown
}
} else {
(exosuit, subtype)
}
} else {
player.ResistArmMotion(Player.neverRestrict)
true
})
) {
(exosuit, subtype)
log.warn(
s"${player.Name} no longer has permission to wear the exo-suit type $exosuit; will wear ${fallbackSuit._1} instead"
)
fallbackSuit
}
}
if (nextSuit == ExoSuitType.MAX) {
player.ResistArmMotion(PlayerControl.maxRestriction)
} else {
log.warn(
s"${player.Name} no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead"
)
(fallbackSuit, fallbackSubtype)
player.ResistArmMotion(Player.neverRestrict)
}
//sanitize (incoming) inventory
//TODO equipment permissions; these loops may be expanded upon in future
@ -802,7 +810,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//choose
if (target.Health > 0) {
//alive
if (target.Health <= 25 && target.Health + damageToHealth > 25 &&
if (target.Health <= 25 &&
(player.avatar.implants.flatten.find { _.definition.implantType == ImplantType.SecondWind } match {
case Some(wind) => wind.initialized
case _ => false
@ -810,8 +818,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//activate second wind
player.Health += 25
player.LogActivity(HealFromImplant(ImplantType.SecondWind, 25))
avatarActor ! AvatarActor.ResetImplant(ImplantType.SecondWind)
avatarActor ! AvatarActor.RestoreStamina(25)
avatarActor ! AvatarActor.ResetImplant(ImplantType.SecondWind)
}
//take damage/update
DamageAwareness(target, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor)
@ -1025,21 +1033,23 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
//TODO other methods of death?
val pentry = PlayerSource(target)
(cause.adversarial match {
case out @ Some(_) =>
out
case _ =>
cause
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
.orElse {
target.LastDamage.collect {
case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis =>
attack.adversarial
attack
.adversarial
.collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out }
}.flatten
}) match {
} match {
case Some(adversarial) =>
events ! AvatarServiceMessage(
zoneChannel,
AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement)
)
case None =>
case _ =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
}
@ -1288,6 +1298,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
suicide()
}
//noinspection ScalaUnusedSymbol
def doInteractingWithGantryField(
obj: PlanetSideServerObject,
body: PieceOfEnvironment,

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 PSForever
package net.psforever.objects.ce
import akka.actor.{ActorContext, Cancellable}
import akka.actor.ActorContext
import net.psforever.objects.{Default, TelepadDeployable, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.structures.Amenity
@ -147,6 +147,12 @@ object TelepadLike {
LocalAction.SendResponse(GenericObjectActionMessage(telepadGUID, 28))
)
}
def InitializeTelepadDeployable(zone: Zone, internal: InternalTelepad, pad: TelepadDeployable): Unit = {
internal.Telepad = pad.GUID
TelepadLike.StartRouterInternalTelepad(zone, internal.Owner.GUID, internal)
pad.Actor ! TelepadLike.Activate(internal)
}
}
/**
@ -157,8 +163,6 @@ object TelepadLike {
* @param obj an entity that extends `TelepadLike`
*/
class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
var setup: Cancellable = Default.Cancellable
def receive: akka.actor.Actor.Receive = {
case TelepadLike.Activate(o: InternalTelepad) if obj eq o =>
obj.Active = true
@ -166,11 +170,9 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
case TelepadLike.Deactivate(o: InternalTelepad) if obj eq o =>
obj.Active = false
val zone = obj.Zone
zone.GUID(obj.Telepad) match {
case Some(oldTpad: TelepadDeployable)
if !obj.Active && !setup.isCancelled =>
zone.GUID(obj.Telepad).collect {
case oldTpad: TelepadDeployable if oldTpad.Active =>
oldTpad.Actor ! TelepadLike.SeverLink(obj)
case _ => ;
}
obj.Telepad = None
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendResponse(ObjectDeleteMessage(obj.GUID, 0)))
@ -178,15 +180,20 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
case TelepadLike.RequestLink(tpad: TelepadDeployable) =>
val zone = obj.Zone
if (obj.Active) {
zone.GUID(obj.Telepad) match {
case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled =>
oldTpad.Actor ! TelepadLike.SeverLink(obj)
case _ => ;
}
obj.Telepad = tpad.GUID
//zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.StartRouterInternalTelepad(obj.Owner.GUID, obj.GUID, obj))
TelepadLike.StartRouterInternalTelepad(zone, obj.Owner.GUID, obj)
tpad.Actor ! TelepadLike.Activate(obj)
zone
.GUID(obj.Telepad)
.collect {
case oldTpad: TelepadDeployable if oldTpad eq tpad =>
Some(oldTpad)
case oldTpad: TelepadDeployable if oldTpad.Active =>
oldTpad.Actor ! TelepadLike.SeverLink(obj)
TelepadLike.InitializeTelepadDeployable(zone, obj, tpad)
Some(oldTpad)
}
.orElse {
TelepadLike.InitializeTelepadDeployable(zone, obj, tpad)
None
}
} else {
val channel = obj.Owner.asInstanceOf[Vehicle].OwnerName.getOrElse("")
zone.LocalEvents ! LocalServiceMessage(channel, LocalAction.RouterTelepadMessage("@Teleport_NotDeployed"))
@ -200,6 +207,6 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor {
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendResponse(ObjectDeleteMessage(obj.GUID, 0)))
}
case _ => ;
case _ => ()
}
}

View file

@ -36,7 +36,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
health,
unk4 = false,
no_mount_points = false,
obj.DeploymentState,
SterilizedDeploymentState(obj),
unk5 = false,
unk6 = false,
obj.Cloaked,
@ -105,6 +105,13 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
.toList
}
private def SterilizedDeploymentState(obj: Vehicle): DriveState.Value = {
obj.DeploymentState match {
case state if state.id < 0 => DriveState.Mobile
case state => state
}
}
protected def SpecificFormatModifier: VehicleFormat.Value = VehicleFormat.Normal
protected def SpecificFormatData(obj: Vehicle): Option[SpecificVehicleData] = None

View file

@ -59,7 +59,9 @@ object AmenitySource {
)
amenity.copy(occupants = obj match {
case o: Mountable =>
o.Seats.values.flatMap { _.occupants }.map { p => PlayerSource.inSeat(p, o, amenity) }.toList
o.Seats
.collect { case (num, seat) if seat.isOccupied => (num, seat.occupants.head) }
.map { case (num, p) => PlayerSource.inSeat(p, amenity, num) }.toList
case _ =>
Nil
})

View file

@ -109,17 +109,17 @@ object PlayerSource {
* even if this function is entirely for the purpose of establishing that the player is an occupant of the mountable entity.<br>
* Don't think too much about it.
* @param player player
* @param mount mountable entity in which the player should be seated
* @param source a `SourceEntry` for the aforementioned mountable entity
* @param seatNumber the attributed seating index in which the player is mounted in `source`
* @return a `PlayerSource` entity
*/
def inSeat(player: Player, mount: Mountable, source: SourceEntry): PlayerSource = {
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit
val faction = player.Faction
PlayerSource(
player.Definition,
exosuit,
Some((source, mount.PassengerInSeat(player).get)),
Some((source, seatNumber)),
player.Health,
player.Armor,
player.Position,

View file

@ -57,7 +57,9 @@ object TurretSource {
)
turret.copy(occupants = obj match {
case o: Mountable =>
o.Seats.values.flatMap { _.occupants }.map { p => PlayerSource.inSeat(p, o, turret) }.toList
o.Seats
.collect { case (num, seat) if seat.isOccupied => (num, seat.occupants.head) }
.map { case (num, p) => PlayerSource.inSeat(p, turret, num) }.toList
case _ =>
Nil
})

View file

@ -53,9 +53,9 @@ object VehicleSource {
)
)
vehicle.copy(occupants = {
obj.Seats.values.map { seat =>
obj.Seats.map { case (seatNumber, seat) =>
seat.occupant match {
case Some(p) => PlayerSource.inSeat(p, obj, vehicle) //shallow
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber) //shallow
case _ => PlayerSource.Nobody
}
}.toList

View file

@ -50,7 +50,7 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
def UpdateNtuUI(vehicle: Vehicle with NtuContainer): Unit = {
if (vehicle.Seats.values.exists(_.isOccupied)) {
val display = scala.math.ceil(vehicle.NtuCapacitorScaled).toLong
val display = vehicle.NtuCapacitorScaled.toLong
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
vehicle.Actor.toString,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 45, display)

View file

@ -19,7 +19,7 @@ import scala.concurrent.duration._
class AntControl(vehicle: Vehicle)
extends DeployingVehicleControl(vehicle)
with AntTransferBehavior {
def ChargeTransferObject = vehicle
def ChargeTransferObject: Vehicle = vehicle
override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(antBehavior)
@ -38,7 +38,7 @@ class AntControl(vehicle: Vehicle)
vehicle.Actor,
TransferBehavior.Charging(Ntu.Nanites)
)
case _ => ;
case _ => ()
}
}
@ -51,7 +51,7 @@ class AntControl(vehicle: Vehicle)
state match {
case DriveState.Undeploying =>
TryStopChargingEvent(vehicle)
case _ => ;
case _ => ()
}
}
}

View file

@ -20,7 +20,7 @@ import net.psforever.types._
class DeployingVehicleControl(vehicle: Vehicle)
extends VehicleControl(vehicle)
with DeploymentBehavior {
def DeploymentObject = vehicle
def DeploymentObject: Vehicle = vehicle
override def commonEnabledBehavior : Receive = super.commonEnabledBehavior.orElse(deployBehavior)
@ -45,7 +45,7 @@ class DeployingVehicleControl(vehicle: Vehicle)
override def commonDeleteBehavior : Receive =
super.commonDeleteBehavior
.orElse {
case msg : Deployment.TryUndeploy =>
case msg: Deployment.TryUndeploy =>
deployBehavior.apply(msg)
}
@ -53,7 +53,7 @@ class DeployingVehicleControl(vehicle: Vehicle)
* Even when disabled, the vehicle can be made to undeploy.
*/
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
TryUndeployStateChange(DriveState.Undeploying)
super.PrepareForDisabled(kickPassengers)
}

View file

@ -24,11 +24,10 @@ class RouterControl(vehicle: Vehicle)
override def specificResponseToDeployment(state: DriveState.Value): Unit = {
state match {
case DriveState.Deployed =>
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Activate(util)
case _ => ;
vehicle.Utility(UtilityType.internal_router_telepad_deployable).collect {
case util: Utility.InternalTelepad => util.Actor ! TelepadLike.Activate(util)
}
case _ => ;
case _ => ()
}
}
@ -40,11 +39,10 @@ class RouterControl(vehicle: Vehicle)
override def specificResponseToUndeployment(state: DriveState.Value): Unit = {
state match {
case DriveState.Undeploying =>
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) => util.Actor ! TelepadLike.Deactivate(util)
case _ => ;
vehicle.Utility(UtilityType.internal_router_telepad_deployable).collect {
case util: Utility.InternalTelepad => util.Actor ! TelepadLike.Deactivate(util)
}
case _ => ;
case _ => ()
}
}
}

View file

@ -13,6 +13,7 @@ import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObjectControl}
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.hackable.GenericHackables
@ -120,6 +121,9 @@ class VehicleControl(vehicle: Vehicle)
case Vehicle.Ownership(Some(player)) =>
GainOwnership(player)
case Mountable.TryMount(user, mountPoint) if vehicle.DeploymentState == DriveState.AutoPilot =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
case msg @ Mountable.TryMount(player, mount_point) =>
mountBehavior.apply(msg)
mountCleanup(mount_point, player)
@ -864,10 +868,15 @@ class VehicleControl(vehicle: Vehicle)
)
}
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
passengerRadiationCloudTimer.cancel()
super.DestructionAwareness(target, cause)
}
}
object VehicleControl {
import net.psforever.objects.vital.{ShieldCharge}
import net.psforever.objects.vital.ShieldCharge
private case class PrepareForDeletion()

View file

@ -31,7 +31,8 @@ import scodec.codecs._
* For flight vehicles, for `n`, the forward air speed for the value in this packet will be at least `1.18 * n`.
* This approximation is not always going to be accurate but serves as a good rule of thumb.
* @param lock_accelerator driver has no control over vehicle acceleration
* @param lock_wheel driver has no control over vehicle turning
* @param lock_wheel driver has no control over vehicle turning;
* generally, the driver never has turning control
* @param reverse move in reverse
* 0 = forward
* 1 = reverse

View file

@ -21,5 +21,7 @@ object DriveState extends Enumeration {
val State7 = Value(7) //unknown; not encountered on a vehicle that can deploy; functions like Mobile
val State127 = Value(127) //unknown
val Kneeling = Value(-1) //flag bfr kneeling state; should not not encode
//the following values should never be encoded
val Kneeling = Value(-1) //flag bfr kneeling state
val AutoPilot = Value(-2) //when emerging from spawn pad, or being kicked from a ferry, during server guidance
}

View file

@ -154,6 +154,7 @@ case class GameConfig(
newAvatar: NewAvatar,
hart: HartConfig,
sharedMaxCooldown: Boolean,
sharedBfrCooldown: Boolean,
baseCertifications: Seq[Certification],
warpGates: WarpGateConfig,
cavernRotation: CavernRotationConfig,

View file

@ -32,7 +32,7 @@ import org.specs2.mutable.Specification
import scala.concurrent.duration._
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalMech, ImplantTerminalMechControl}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.projectile.ProjectileReason
@ -327,21 +327,17 @@ class DamageableEntityDamageTest extends ActorTest {
gen.Actor ! Vitality.Damage(applyDamageTo)
val msg1 = avatarProbe.receiveOne(500 milliseconds)
val msg2 = activityProbe.receiveOne(500 milliseconds)
assert(
msg1 match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
assert(
msg2 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == SourceEntry(gen) &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
)
msg1 match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(ValidPlanetSideGUID(2), 0, 3600)) => ()
case _ => assert(false, "DamageableEntity:handle taking damage - player not messaged")
}
msg2 match {
case activity: Zone.HotSpot.Activity
if activity.attacker == PlayerSource(player1) &&
activity.defender == SourceEntry(gen) &&
activity.location == Vector3(1, 0, 0) => ()
case _ => assert(false, "DamageableEntity:handle taking damage - activity not messaged")
}
}
}
}

View file

@ -320,15 +320,12 @@ class DeployableBehaviorDeconstructOwnedTest extends FreedContextActorTest {
jmine.Actor ! Deployable.Deconstruct()
val eventsMsgs = eventsProbe.receiveN(3, 10.seconds)
eventsMsgs.head match {
case LocalServiceMessage("test", LocalAction.EliminateDeployable(`jmine`, PlanetSideGUID(1), Vector3(1,2,3), 2)) => ;
case LocalServiceMessage("test",LocalAction.EliminateDeployable(mine, ValidPlanetSideGUID(1), Vector3(1.0,2.0,3.0),2))
if mine eq jmine => ;
case _ => assert(false, "owned deconstruct test - not eliminating deployable")
}
eventsMsgs(1) match {
case LocalServiceMessage("TestCharacter1", LocalAction.DeployableUIFor(DeployedItem.jammer_mine)) => ;
case _ => assert(false, "")
}
eventsMsgs(2) match {
case LocalServiceMessage(
case LocalServiceMessage(
"TR",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
@ -338,6 +335,10 @@ class DeployableBehaviorDeconstructOwnedTest extends FreedContextActorTest {
) => ;
case _ => assert(false, "owned deconstruct test - not removing icon")
}
eventsMsgs(2) match {
case LocalServiceMessage("TestCharacter1", LocalAction.DeployableUIFor(DeployedItem.jammer_mine)) => ;
case _ => assert(false, "")
}
assert(deployableList.isEmpty, "owned deconstruct test - deployable still in list")
assert(!avatar.deployables.Contains(jmine), "owned deconstruct test - avatar still owns deployable")

View file

@ -25,6 +25,7 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.services.local.LocalAction.DeployableMapIcon
import scala.collection.mutable
import scala.concurrent.duration._
@ -447,9 +448,9 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
assert(!h_mine.Destroyed)
h_mine.Actor ! Vitality.Damage(applyDamageToH)
val eventMsgs = eventsProbe.receiveN(5, 200 milliseconds)
val eventMsgs = eventsProbe.receiveN(4, 200 milliseconds)
val p1Msgs = player1Probe.receiveN(1, 200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
val p2Msgs = player2Probe.receiveN(1, 200 milliseconds)
eventMsgs.head match {
case Zone.HotSpot.Conflict(target, attacker, _)
if (target.Definition eq h_mine.Definition) && (attacker eq pSource) => ;
@ -461,10 +462,6 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
case _ => assert(false, "")
}
eventMsgs(2) match {
case LocalServiceMessage("TestCharacter2", LocalAction.DeployableUIFor(DeployedItem.he_mine)) => ;
case _ => assert(false, "")
}
eventMsgs(3) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
@ -475,7 +472,7 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
) => ;
case _ => assert(false, "")
}
eventMsgs(4) match {
eventMsgs(3) match {
case AvatarServiceMessage(
"test",
AvatarAction.Destroy(PlanetSideGUID(2), PlanetSideGUID(3), Service.defaultPlayerGUID, Vector3.Zero)
@ -486,7 +483,10 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
case Vitality.Damage(_) => ;
case _ => assert(false, "")
}
assert(!avatar2.deployables.Contains(h_mine))
p2Msgs.head match {
case Player.LoseDeployable(_) => ;
case _ => assert(false, "")
}
assert(h_mine.Destroyed)
}
}
@ -558,14 +558,10 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
assert(!h_mine.Destroyed)
h_mine.Actor ! Vitality.Damage(applyDamageTo)
val eventMsgs = eventsProbe.receiveN(4, 200 milliseconds)
val eventMsgs = eventsProbe.receiveN(3, 200 milliseconds)
player1Probe.expectNoMessage(200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
val p2Msgs = player2Probe.receiveN(1, 200 milliseconds)
eventMsgs.head match {
case LocalServiceMessage("TestCharacter2", LocalAction.DeployableUIFor(DeployedItem.he_mine)) => ;
case _ => assert(false, "")
}
eventMsgs(1) match {
case LocalServiceMessage(
"NC",
LocalAction.DeployableMapIcon(
@ -576,17 +572,21 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
) => ;
case _ => assert(false, "")
}
eventMsgs(2) match {
eventMsgs(1) match {
case AvatarServiceMessage(
"test",
AvatarAction.Destroy(PlanetSideGUID(2), PlanetSideGUID(3), Service.defaultPlayerGUID, Vector3.Zero)
"test",
AvatarAction.Destroy(PlanetSideGUID(2), PlanetSideGUID(3), Service.defaultPlayerGUID, Vector3.Zero)
) => ;
case _ => assert(false, "")
}
eventMsgs(3) match {
eventMsgs(2) match {
case LocalServiceMessage("test", LocalAction.TriggerEffect(_, "detonate_damaged_mine", PlanetSideGUID(2))) => ;
case _ => assert(false, "")
}
p2Msgs.head match {
case Player.LoseDeployable(_) => ;
case _ => assert(false, "")
}
assert(h_mine.Health <= h_mine.Definition.DamageDestroysAt)
assert(h_mine.Destroyed)
}