From 010b1c5845617d0a29e595a5764fc81625f3c644 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 9 May 2021 07:17:49 -0400 Subject: [PATCH] Better Respawn (#810) * biolab spawn benefit should now be active; spawn point integrity at point of player avatar creation, or else try again * streamlined the process of determining whether a facility possesses individual benefits; warp gates do not have benefits * fix for mounted turret persistance --- .../actors/session/SessionActor.scala | 148 ++++++++++++------ .../serverobject/structures/Building.scala | 138 ++++++++++------ .../serverobject/structures/WarpGate.scala | 5 + 3 files changed, 196 insertions(+), 95 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 626f649a..285237cb 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -200,6 +200,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var lastTerminalOrderFulfillment: Boolean = true var shiftPosition: Option[Vector3] = None var shiftOrientation: Option[Vector3] = None + var nextSpawnPoint: Option[SpawnPoint] = None var setupAvatarFunc: () => Unit = AvatarCreate var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally var persist: () => Unit = NoPersistence @@ -509,14 +510,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(0) => deadState = DeadState.Release // cancel movement updates vehicle.Position = position - LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0 seconds) + LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0 seconds, None) case _ => // not seated as the driver, in which case we can't move } case None => deadState = DeadState.Release // cancel movement updates player.Position = position // continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID)) - LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0 seconds) + LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0 seconds, None) case _ => // seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move } } @@ -530,7 +531,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(0) => deadState = DeadState.Release // cancel movement updates vehicle.Position = position - LoadZonePhysicalSpawnPoint(continent.id, position, Vector3.z(vehicle.Orientation.z), 0 seconds) + LoadZonePhysicalSpawnPoint(continent.id, position, Vector3.z(vehicle.Orientation.z), 0 seconds, None) case _ => // not seated as the driver, in which case we can't move } case None => @@ -610,7 +611,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system - LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds) + LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None) case _ => interstellarFerry match { case None => @@ -1003,16 +1004,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con response match { case Some((zone, spawnPoint)) => val obj = continent.GUID(player.VehicleSeated) match { - case Some(obj: Vehicle) if !obj.Destroyed => - obj - case _ => - player + case Some(obj: Vehicle) if !obj.Destroyed => obj + case _ => player } val (pos, ori) = spawnPoint.SpecificPoint(obj) if (previousZoningType == Zoning.Method.InstantAction) - LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds) + LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint)) else - LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id)) + LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint)) case None => log.warn( s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService; sending home" @@ -1300,37 +1299,58 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con session.zone.map.checksum ) ) - //important! the LoadMapMessage must be processed by the client before the avatar is created - setupAvatarFunc() - //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable - turnCounterFunc = interimUngunnedVehicle match { - case Some(_) => - TurnCounterDuringInterimWhileInPassengerSeat - case None => - TurnCounterDuringInterim + if (isAcceptableNextSpawnPoint()) { + //important! the LoadMapMessage must be processed by the client before the avatar is created + setupAvatarFunc() + //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable + turnCounterFunc = interimUngunnedVehicle match { + case Some(_) => + TurnCounterDuringInterimWhileInPassengerSeat + case None => + TurnCounterDuringInterim + } + keepAliveFunc = NormalKeepAlive + upstreamMessageCount = 0 + setAvatar = false + persist() + } else { + //look for different spawn point in same zone + cluster ! ICS.GetNearbySpawnPoint( + session.zone.Number, + tplayer, + Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS), + context.self + ) } - keepAliveFunc = NormalKeepAlive - upstreamMessageCount = 0 - setAvatar = false - persist() case PlayerLoaded(tplayer) => //same zone log.info(s"${tplayer.Name} will respawn") tplayer.avatar = avatar session = session.copy(player = tplayer) - setupAvatarFunc() - //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable - turnCounterFunc = interimUngunnedVehicle match { - case Some(_) => - TurnCounterDuringInterimWhileInPassengerSeat - case None => - TurnCounterDuringInterim + if (isAcceptableNextSpawnPoint()) { + //try this spawn point + setupAvatarFunc() + //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable + turnCounterFunc = interimUngunnedVehicle match { + case Some(_) => + TurnCounterDuringInterimWhileInPassengerSeat + case None => + TurnCounterDuringInterim + } + keepAliveFunc = NormalKeepAlive + upstreamMessageCount = 0 + setAvatar = false + persist() + } else { + //look for different spawn point in same zone + cluster ! ICS.GetNearbySpawnPoint( + continent.Number, + tplayer, + Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS), + context.self + ) } - keepAliveFunc = NormalKeepAlive - upstreamMessageCount = 0 - setAvatar = false - persist() case PlayerFailedToLoad(tplayer) => player.Continent match { @@ -1386,7 +1406,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.Population ! Zone.Population.Leave(avatar) //does not matter if it doesn't work zoneLoaded = None zoneReload = true - LoadZonePhysicalSpawnPoint(toZoneId, pos, orient, 0 seconds) + LoadZonePhysicalSpawnPoint(toZoneId, pos, orient, respawnTime = 0 seconds, None) } } else if (tplayer.isAlive) { if ( @@ -1399,7 +1419,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (!setAvatar || waitingOnUpstream) { setCurrentAvatarFunc(tplayer) respawnTimer = context.system.scheduler.scheduleOnce( - delay = (if (attempt <= max_attempts / 2) 10 else 5) seconds, + delay = (if (attempt <= max_attempts / 2) 10 + else 5) seconds, self, SetCurrentAvatar(tplayer, max_attempts, attempt + max_attempts / 3) ) @@ -1408,8 +1429,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case (Some(v: Vehicle), Some(seatNumber)) if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty => KeepAlivePersistence - case _ => NormalKeepAlive + case _ => + NormalKeepAlive } + nextSpawnPoint = None } //if not the condition above, player has started playing normally } else { @@ -1693,7 +1716,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con interstellarFerry = Some(droppod) //leverage vehicle gating player.Position = droppod.Position player.VehicleSeated = PlanetSideGUID(0) - LoadZonePhysicalSpawnPoint(zone.id, droppod.Position, Vector3.Zero, 0 seconds) + LoadZonePhysicalSpawnPoint(zone.id, droppod.Position, Vector3.Zero, 0 seconds, None) } /** @@ -2571,7 +2594,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) UpdateWeaponAtSeatPosition(obj, seat_number) MountingAction(tplayer, obj, seat_number) - keepAliveFunc = KeepAlivePersistence case Mountable.CanMount(obj: Mountable, _, _) => log.warn(s"MountVehicleMsg: $obj is some mountable object and nothing will happen for ${player.Name}") @@ -3948,11 +3970,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con FindContainedWeapon match { case (Some(o), Some(tool)) => (o match { - case mount: Mountable => mount.PassengerInSeat(player) - case _ => None + case mount: Mountable => (o, mount.PassengerInSeat(player)) + case _ => (None, None) }) match { - case None | Some(0) => ; - case Some(_) => + case (None, None) | (_, None) | (_: Vehicle, Some(0)) => ; + case _ => persist() turnCounterFunc(player.GUID) } @@ -8593,7 +8615,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param respawnTime the character downtime spent respawning, as clocked on the redeployment screen; * does not factor in any time required for loading zone or game objects */ - def LoadZonePhysicalSpawnPoint(zoneId: String, pos: Vector3, ori: Vector3, respawnTime: FiniteDuration): Unit = { + def LoadZonePhysicalSpawnPoint( + zoneId: String, + pos: Vector3, + ori: Vector3, + respawnTime: FiniteDuration, + physSpawnPoint: Option[SpawnPoint] + ): Unit = { log.info(s"${player.Name} will load in zone $zoneId at position $pos in $respawnTime") respawnTimer.cancel() reviveTimer.cancel() @@ -8608,6 +8636,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk5 = true ) ) + nextSpawnPoint = physSpawnPoint shiftPosition = Some(pos) shiftOrientation = Some(ori) @@ -9128,6 +9157,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** * Given an origin and a destination, determine how long the process of traveling should take in reconstruction time. * For most destinations, the unit of receiving ("spawn point") determines the reconstruction time. + * Possession of a lattice-linked friendly Bio Laboratory halves the time of spawning at facilities. * In a special consideration, travel to any sanctuary or sanctuary-special zone should be as immediate as zone loading. * * @param toZoneId the zone where the target is headed @@ -9137,12 +9167,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def CountSpawnDelay(toZoneId: String, toSpawnPoint: SpawnPoint, fromZoneId: String): FiniteDuration = { val sanctuaryZoneId = Zones.sanctuaryZoneId(player.Faction) - if (fromZoneId.equals("Nowhere") || sanctuaryZoneId.equals(toZoneId)) { //to sanctuary + if (fromZoneId.equals("Nowhere") || sanctuaryZoneId.equals(toZoneId) || !isAcceptableNextSpawnPoint()) { + //first login, to sanctuary, resolution of invalid spawn point 0 seconds - } else if (!player.isAlive) { - toSpawnPoint.Definition.Delay seconds //TODO +cumulative death penalty } else { - toSpawnPoint.Definition.Delay seconds + //for other zones ... + //biolabs have/grant benefits + val cryoBenefit: Float = toSpawnPoint.Owner match { + case b: Building if b.hasLatticeBenefit(GlobalDefinitions.cryo_facility) => 0.5f + case _ => 1f + } + //TODO cumulative death penalty + toSpawnPoint.Definition.Delay.toFloat * cryoBenefit seconds } } @@ -9714,6 +9750,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } + def isAcceptableNextSpawnPoint(): Boolean = isAcceptableSpawnPoint(nextSpawnPoint) + + def isAcceptableSpawnPoint(spawnPoint: SpawnPoint): Boolean = isAcceptableSpawnPoint(Some(spawnPoint)) + + def isAcceptableSpawnPoint(spawnPoint: Option[SpawnPoint]): Boolean = { + spawnPoint match { + case Some(aSpawnPoint) => + !aSpawnPoint.isOffline && + (aSpawnPoint.Owner match { + case w: WarpGate => w.Active + case b: Building => b.Faction == player.Faction + case v: Vehicle => v.Faction == player.Faction && !v.Destroyed && v.DeploymentState == DriveState.Deployed + case _ => true + }) + case None => true + } + } + def failWithError(error: String) = { log.error(error) middlewareActor ! MiddlewareActor.Teardown() diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index dfb5064a..940fc477 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -182,53 +182,6 @@ class Building( (o.nonEmpty, false) //TODO poll pain field strength } - val latticeBenefit: Int = { - if (Faction == PlanetSideEmpire.NEUTRAL) 0 - else { - def FindLatticeBenefit( - wantedBenefit: ObjectDefinition, - subGraph: Graph[Building, GraphEdge.UnDiEdge] - ): Boolean = { - var found = false - - subGraph find this match { - case Some(self) => - if (this.Definition == wantedBenefit) found = true - else { - self pathUntil (_.Definition == wantedBenefit) match { - case Some(_) => found = true - case None => ; - } - } - case None => ; - } - - found - } - - // Check this Building is on the lattice first - zone.Lattice find this match { - case Some(_) => - val subGraph = Zone.Lattice filter ((b: Building) => - b.Faction == this.Faction - && !b.CaptureTerminalIsHacked - && b.NtuLevel > 0 - && (b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed) - ) - - var stackedBenefit = 0 - if (FindLatticeBenefit(GlobalDefinitions.amp_station, subGraph)) stackedBenefit |= 1 - if (FindLatticeBenefit(GlobalDefinitions.comm_station_dsp, subGraph)) stackedBenefit |= 2 - if (FindLatticeBenefit(GlobalDefinitions.cryo_facility, subGraph)) stackedBenefit |= 4 - if (FindLatticeBenefit(GlobalDefinitions.comm_station, subGraph)) stackedBenefit |= 8 - if (FindLatticeBenefit(GlobalDefinitions.tech_plant, subGraph)) stackedBenefit |= 16 - - stackedBenefit - case None => 0; - } - } - } - BuildingInfoUpdateMessage( Zone.Number, MapId, @@ -242,7 +195,7 @@ class Building( generatorState, spawnTubesNormal, forceDomeActive, - if (generatorState != PlanetSideGeneratorState.Destroyed) latticeBenefit else 0, + if (generatorState != PlanetSideGeneratorState.Destroyed) latticeBenefitsValue() else 0, if (generatorState != PlanetSideGeneratorState.Destroyed) 48 else 0, // cavern benefit Nil, // unk4, 0, // unk5 @@ -254,6 +207,95 @@ class Building( ) } + def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = { + if (Faction == PlanetSideEmpire.NEUTRAL) { + false + } else { + // Check this Building is on the lattice first + zone.Lattice find this match { + case Some(_) => + val subGraph = Zone.Lattice filter ( + (b : Building) => + b.Faction == this.Faction && + !b.CaptureTerminalIsHacked && + b.NtuLevel > 0 && + (b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed) + ) + findLatticeBenefit(wantedBenefit, subGraph) + case None => + false + } + } + } + + private def findLatticeBenefit( + wantedBenefit: ObjectDefinition, + subGraph: Graph[Building, GraphEdge.UnDiEdge] + ): Boolean = { + var found = false + subGraph find this match { + case Some(self) => + if (this.Definition == wantedBenefit) { + found = true + } else { + self pathUntil (_.Definition == wantedBenefit) match { + case Some(_) => found = true + case None => ; + } + } + case None => ; + } + found + } + + def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = { + if (Faction == PlanetSideEmpire.NEUTRAL) { + Set.empty + } else { + // Check this Building is on the lattice first + zone.Lattice find this match { + case Some(_) => + val subGraph = Zone.Lattice filter ((b: Building) => + b.Faction == this.Faction + && !b.CaptureTerminalIsHacked + && b.NtuLevel > 0 + && (b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed) + ) + + import scala.collection.mutable + var connectedBases: mutable.Set[ObjectDefinition] = mutable.Set() + if (findLatticeBenefit(GlobalDefinitions.amp_station, subGraph)) { + connectedBases.add(GlobalDefinitions.amp_station) + } + if (findLatticeBenefit(GlobalDefinitions.comm_station_dsp, subGraph)) { + connectedBases.add(GlobalDefinitions.comm_station_dsp) + } + if (findLatticeBenefit(GlobalDefinitions.cryo_facility, subGraph)) { + connectedBases.add(GlobalDefinitions.cryo_facility) + } + if (findLatticeBenefit(GlobalDefinitions.comm_station, subGraph)) { + connectedBases.add(GlobalDefinitions.comm_station) + } + if (findLatticeBenefit(GlobalDefinitions.tech_plant, subGraph)) { + connectedBases.add(GlobalDefinitions.tech_plant) + } + connectedBases.toSet + case None => + Set.empty + } + } + } + + def latticeBenefitsValue(): Int = { + latticeConnectedFacilityBenefits().collect { + case GlobalDefinitions.amp_station => 1 + case GlobalDefinitions.comm_station_dsp => 2 + case GlobalDefinitions.cryo_facility => 4 + case GlobalDefinitions.comm_station => 8 + case GlobalDefinitions.tech_plant => 16 + }.sum + } + def BuildingType: StructureType = buildingType override def Zone_=(zone: Zone): Zone = Zone //building never leaves zone after being set in constructor diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala b/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala index 07cc3d68..75010ffa 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala @@ -9,6 +9,7 @@ import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3} import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.definition.ObjectDefinition import scala.collection.mutable @@ -154,6 +155,10 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi def NtuCapacitor_=(value: Float): Float = NtuCapacitor + override def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = false + + override def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = Set.empty + override def Definition: WarpGateDefinition = buildingDefinition }