From a1cf6c27018109c7909dbb8ca591a2e56fad4e91 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 22 Apr 2023 00:03:13 -0400 Subject: [PATCH] Reduced Upstream/Downstream Load (#1052) * an attempt to space out the player upstream * some data sterilizing * when a player is respawning, other players do not send their own update PSM's * always display players in their proper locations when nearing their view, even if they have been stationary this whole time * fixing configuration file and warnings --- src/main/resources/application.conf | 35 +++++++ .../actors/session/AvatarActor.scala | 3 + .../support/SessionAvatarHandlers.scala | 96 ++++++++++++++----- .../actors/session/support/SessionData.scala | 35 ++++--- .../services/InterstellarClusterService.scala | 2 + .../scala/net/psforever/util/Config.scala | 15 +++ 6 files changed, 150 insertions(+), 36 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index d4ff70580..66d31a914 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -183,6 +183,41 @@ game { } } + # Limits the dispatch of PlayerStateMessage packets from a session to its client. + # Specifically, a packet will only be dispatched based on whether + # it is to be dispatched over a threshold time after the previous dispatch time. + # The delay between packets is determined by a distance between the observer player and the target player. + # Only affects PlayerStateMessage. + player-draw = { + # Minimum number of players within a given region before scaling down the range. + # Total population - population threshold = overpopulation + population-threshold = 20 + # Number of players over threshold before the reduction in range is scaled down. + # Population step / overpopulation = range step factor (integer) + population-step = 5 + # Always send packets regarding target players within the given distance. + range-min = 50 + # Do not send packets regarding target players beyond this distance. (sq.m) + # Treat this as an absolute render distance. + range-max = 550 + # Number of meters reduced from range based on population count over threshold value. (m) + # Range step * range step factor = total distance to remove from actual distance + range-step = 25 + # The distances above min-range where different delays are allowed before a successful packet must be dispatched. [m] + # Test distances against entries to find the last one that is not more than the sample distance. + # 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 + # 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 + # as the heartbeat of upstream packets are measured in quanta of 250ms usually. + # As a consequence, additional entries with proper time spacing will push back the next proper update considerably + # while additional entries with insufficient time spacing may result in no change in behavior. + delays = [350, 600, 800] + } + doors-can-be-opened-by-med-app-from-this-distance = 5.05 } diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 0889ce3a0..c04691e92 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -1063,6 +1063,9 @@ class AvatarActor( } else { performAvatarLogin(avatarId, account.id, replyTo) } + case Success(_) => + //TODO this may not be an actual failure, but don't know what to do + sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6)) case Failure(e) => log.error(e)("db failure") sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6)) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 5f6d03ce0..bba719639 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -70,34 +70,84 @@ class SessionAvatarHandlers( jumpThrust, isCloaking, spectating, - _ + canSeeReallyFar ) if isNotSameTarget => - val now = System.currentTimeMillis() - val (location, time, distanceSq): (Vector3, Long, Float) = if (spectating) { - val r2 = 2 + hidingPlayerRandomizer.nextInt(4000).toFloat - (Vector3(r2, r2, 1f), 0L, 0f) - } else { - val before = lastSeenStreamMessage(guid.guid).time - val dist = Vector3.DistanceSquared(player.Position, pos) - (pos, now - before, dist) + val pstateToSave = pstate.copy(timestamp = 0) + val (lastMsg, lastTime, lastPosition, wasSpectating, wasVisible) = lastSeenStreamMessage(guid.guid) match { + case SessionAvatarHandlers.LastUpstream(Some(msg), visible, time) => (Some(msg), time, msg.pos, msg.spectator, visible) + case _ => (None, 0L, Vector3.Zero, false, false) } - if (distanceSq < 302500 || time > 5000) { // Render distance seems to be approx 525m. Reduce update rate at ~550m to be safe + val drawConfig = Config.app.game.playerDraw + val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m + val ourPosition = player.Position + val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m + val inVisibleRange = currentDistance <= maxRange + val wasInVisibleRange = Vector3.DistanceSquared(lastPosition, pos) <= maxRange + val comingIntoVisibleRange = inVisibleRange && !wasInVisibleRange + val now = System.currentTimeMillis() //ms + val durationSince = now - lastTime //ms + val released = player.isReleased + if (!released && + !spectating && + (comingIntoVisibleRange || (inVisibleRange && !lastMsg.contains(pstateToSave)))) { + lazy val targetDelay = { + val populationOver = math.max( + 0, + continent.blockMap.sector(ourPosition, range=drawConfig.rangeMax.toFloat).livePlayerList.size - drawConfig.populationThreshold + ) + val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m + val adjustedDistance = currentDistance + distanceAdjustment //sq.m + drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match { + case -1 => 1 + case index => drawConfig.delays(index) + } + } //ms + if (comingIntoVisibleRange || + canSeeReallyFar || + currentDistance < drawConfig.rangeMin * drawConfig.rangeMin || + durationSince > drawConfig.delayMax || + sessionData.canSeeReallyFar || + durationSince > targetDelay) { + //must draw + sendResponse( + PlayerStateMessage( + guid, + pos, + vel, + yaw, + pitch, + yawUpper, + timestamp = 0, //is this okay? + isCrouching, + isJumping, + jumpThrust, + isCloaking + ) + ) + lastSeenStreamMessage(guid.guid) = SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, now) + } else { + lastSeenStreamMessage(guid.guid) = SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, lastTime) + } + } else if ( + (!spectating && wasInVisibleRange && !inVisibleRange) || + (inVisibleRange && !wasSpectating && spectating) || + (wasVisible && released) + ) { + //must hide + val lat = (1 + hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat sendResponse( PlayerStateMessage( guid, - location, - vel, - yaw, - pitch, - yawUpper, - timestamp = 0, - isCrouching, - isJumping, - jumpThrust, - isCloaking + Vector3(1f, lat, 1f), + vel=None, + facingYaw=0f, + facingPitch=0f, + facingYawUpper=0f, + timestamp=0, //is this okay? + is_cloaked = isCloaking ) ) - lastSeenStreamMessage(guid.guid) = SessionAvatarHandlers.LastUpstream(Some(pstate), now) + lastSeenStreamMessage(guid.guid) = SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, now) } case AvatarResponse.ObjectHeld(slot, _) @@ -500,9 +550,9 @@ class SessionAvatarHandlers( } object SessionAvatarHandlers { - private[support] case class LastUpstream(msg: Option[AvatarResponse.PlayerState], time: Long) + private[support] case class LastUpstream(msg: Option[AvatarResponse.PlayerState], visible: Boolean, time: Long) private[support] def blankUpstreamMessages(n: Int): Array[LastUpstream] = { - Array.fill[SessionAvatarHandlers.LastUpstream](n)(elem = LastUpstream(None, 0L)) + Array.fill[SessionAvatarHandlers.LastUpstream](n)(elem = LastUpstream(None, visible=false, 0L)) } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 4bbf960eb..0ad74b3d9 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -2711,19 +2711,28 @@ class SessionData( } def canSeeReallyFar: Boolean = { - findEquipment().exists { - case weapon: Tool - if weapon.Size == EquipmentSize.Rifle && - (weapon.Projectile ne GlobalDefinitions.no_projectile) && - player.Crouching && - player.avatar - .implants - .exists { p => - p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.initialized }.nonEmpty - } => - true - case item => - item.Definition == GlobalDefinitions.bolt_driver || item.Definition == GlobalDefinitions.heavy_sniper + shooting.FindContainedWeapon match { + case (Some(_: Vehicle), weapons) if weapons.nonEmpty => + player.avatar + .implants + .exists { p => + p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty + } + case (Some(_: Player), weapons) if weapons.nonEmpty => + val wep = weapons.head + wep.Definition == GlobalDefinitions.bolt_driver || + wep.Definition == GlobalDefinitions.heavy_sniper || + ( + (wep.Projectile ne GlobalDefinitions.no_projectile) && + player.Crouching && + player.avatar + .implants + .exists { p => + p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty + } + ) + case _ => + false } } diff --git a/src/main/scala/net/psforever/services/InterstellarClusterService.scala b/src/main/scala/net/psforever/services/InterstellarClusterService.scala index a1dca5db9..c95ea702c 100644 --- a/src/main/scala/net/psforever/services/InterstellarClusterService.scala +++ b/src/main/scala/net/psforever/services/InterstellarClusterService.scala @@ -290,6 +290,8 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic cavernRotation.foreach { rotation => rotation ! rotationMsg } + + case _ => () } this } diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index b4fb18fe1..0e372c70f 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -158,6 +158,7 @@ case class GameConfig( warpGates: WarpGateConfig, cavernRotation: CavernRotationConfig, savedMsg: SavedMessageEvents, + playerDraw: PlayerStateDrawSettings, doorsCanBeOpenedByMedAppFromThisDistance: Float ) @@ -220,3 +221,17 @@ case class SavedMessageTimings( fixed: Long, variable: Long ) + +case class PlayerStateDrawSettings( + populationThreshold: Int, + populationStep: Int, + rangeMin: Int, + rangeMax: Int, + rangeStep: Int, + ranges: Seq[Int], + delayMax: Long, + delays: Seq[Long] +) { + assert(ranges.nonEmpty) + assert(ranges.size == delays.size) +}