mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
merge rebase; accommodation for suppressing aura where no aura should be displayed; new radial degrade calculations
This commit is contained in:
parent
ac5e26f37a
commit
e27e827552
|
|
@ -156,4 +156,23 @@ object AggravatedDamage {
|
|||
vanu_aggravated,
|
||||
targets
|
||||
)
|
||||
|
||||
def burning(resolution: ProjectileResolution.Value): ProjectileResolution.Value = {
|
||||
resolution match {
|
||||
case ProjectileResolution.AggravatedDirect => ProjectileResolution.AggravatedDirectBurn
|
||||
case ProjectileResolution.AggravatedSplash => ProjectileResolution.AggravatedSplashBurn
|
||||
case _ => resolution
|
||||
}
|
||||
}
|
||||
|
||||
def basicDamageType(resolution: ProjectileResolution.Value): DamageType.Value = {
|
||||
resolution match {
|
||||
case ProjectileResolution.AggravatedDirect | ProjectileResolution.AggravatedDirectBurn =>
|
||||
DamageType.Direct
|
||||
case ProjectileResolution.AggravatedSplash | ProjectileResolution.AggravatedSplashBurn =>
|
||||
DamageType.Splash
|
||||
case _ =>
|
||||
DamageType.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ object AuraEffectBehavior {
|
|||
|
||||
override def isCancelled : Boolean = timer.isCancelled
|
||||
|
||||
override def cancel: Boolean = timer.cancel
|
||||
override def cancel(): Boolean = timer.cancel()
|
||||
}
|
||||
|
||||
object Entry {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ trait AggravatedBehavior {
|
|||
projectile.profile.Aggravated match {
|
||||
case Some(damage)
|
||||
if projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) &&
|
||||
damage.effect_type != Aura.Nothing &&
|
||||
damage.info.exists(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) &&
|
||||
damage.effect_type != Aura.Nothing &&
|
||||
(projectile.quality == ProjectileQuality.AggravatesTarget ||
|
||||
damage.targets.exists(validation => validation.test(AggravatedObject))) =>
|
||||
TryAggravationEffectActivate(damage, data)
|
||||
|
|
@ -61,7 +62,7 @@ trait AggravatedBehavior {
|
|||
|
||||
private def SetupAggravationEntry(aggravation: AggravatedDamage, data: ResolvedProjectile): Boolean = {
|
||||
val effect = aggravation.effect_type
|
||||
aggravation.info.find(_.damage_type == AggravatedBehavior.basicDamageType(data.resolution)) match {
|
||||
aggravation.info.find(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) match {
|
||||
case Some(info) =>
|
||||
val timing = aggravation.timing
|
||||
val duration = timing.duration
|
||||
|
|
@ -77,7 +78,6 @@ trait AggravatedBehavior {
|
|||
case None =>
|
||||
(1000L, (duration / 1000).toInt)
|
||||
}
|
||||
//val leftoverTime = duration - (tick * iterations)
|
||||
//quality per tick
|
||||
val totalPower = (duration.toFloat / info.infliction_rate).toInt - 1
|
||||
val averagePowerPerTick = totalPower.toFloat / iterations
|
||||
|
|
@ -108,7 +108,7 @@ trait AggravatedBehavior {
|
|||
powerOffset: List[Float]
|
||||
): AggravatedBehavior.Entry = {
|
||||
val aggravatedDamageInfo = ResolvedProjectile(
|
||||
AggravatedBehavior.burning(data.resolution),
|
||||
AggravatedDamage.burning(data.resolution),
|
||||
data.projectile,
|
||||
target,
|
||||
data.damage_model,
|
||||
|
|
@ -168,7 +168,7 @@ trait AggravatedBehavior {
|
|||
def CleanupAggravationTimer(id: Long): Unit = {
|
||||
//remove and cancel timer
|
||||
aggravationToTimer.remove(id) match {
|
||||
case Some(timer) => timer.cancel
|
||||
case Some(timer) => timer.cancel()
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
|
@ -179,9 +179,9 @@ trait AggravatedBehavior {
|
|||
}
|
||||
|
||||
def EndAllAggravation(): Unit = {
|
||||
entryIdToEntry.clear
|
||||
aggravationToTimer.values.foreach { _.cancel }
|
||||
aggravationToTimer.clear
|
||||
entryIdToEntry.clear()
|
||||
aggravationToTimer.values.foreach { _.cancel() }
|
||||
aggravationToTimer.clear()
|
||||
}
|
||||
|
||||
def AggravatedReaction: Boolean = ongoingAggravated
|
||||
|
|
@ -206,23 +206,4 @@ object AggravatedBehavior {
|
|||
private case class Entry(id: Long, effect: Aura, retime: Long, data: ResolvedProjectile, qualityPerTick: List[Float])
|
||||
|
||||
private case class Aggravate(id: Long, iterations: Int)
|
||||
|
||||
private def burning(resolution: ProjectileResolution.Value): ProjectileResolution.Value = {
|
||||
resolution match {
|
||||
case ProjectileResolution.AggravatedDirect => ProjectileResolution.AggravatedDirectBurn
|
||||
case ProjectileResolution.AggravatedSplash => ProjectileResolution.AggravatedSplashBurn
|
||||
case _ => resolution
|
||||
}
|
||||
}
|
||||
|
||||
private def basicDamageType(resolution: ProjectileResolution.Value): DamageType.Value = {
|
||||
resolution match {
|
||||
case ProjectileResolution.AggravatedDirect | ProjectileResolution.AggravatedDirectBurn =>
|
||||
DamageType.Direct
|
||||
case ProjectileResolution.AggravatedSplash | ProjectileResolution.AggravatedSplashBurn =>
|
||||
DamageType.Splash
|
||||
case _ =>
|
||||
DamageType.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
package net.psforever.actors.session
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
//language imports
|
||||
import akka.actor.typed
|
||||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import MDCContextAware.Implicits._
|
||||
import org.log4s.MDC
|
||||
import scala.collection.mutable.LongMap
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success}
|
||||
import scala.util.Success
|
||||
import scodec.bits.ByteVector
|
||||
import services.properties.PropertyOverrideManager
|
||||
import org.joda.time.{LocalDateTime, Period}
|
||||
import csr.{CSRWarp, CSRZone, Traveler}
|
||||
import MDCContextAware.Implicits._
|
||||
import net.psforever.objects.{GlobalDefinitions, _}
|
||||
import net.psforever.objects.avatar.{Avatar, Certification, DeployableToolbox}
|
||||
//project imports
|
||||
import net.psforever.login.{DropCryptoSession, DropSession, HelloFriend, RawPacket}
|
||||
import net.psforever.login.WorldSession._
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar.{Certification, DeployableToolbox, FirstTimeEvents}
|
||||
import net.psforever.objects.avatar.{Avatar, Certification, Cosmetic, DeployableToolbox}
|
||||
import net.psforever.objects.ballistics._
|
||||
import net.psforever.objects.ce._
|
||||
import net.psforever.objects.definition._
|
||||
|
|
@ -29,6 +29,7 @@ import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
|
|||
import net.psforever.objects.equipment.{EffectTarget, Equipment, FireModeSwitch, JammableUnit}
|
||||
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
|
|
@ -48,7 +49,6 @@ import net.psforever.objects.serverobject.terminals._
|
|||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
|
||||
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.teamwork.Squad
|
||||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vehicles.Utility.InternalTelepad
|
||||
|
|
@ -56,21 +56,12 @@ import net.psforever.objects.vital._
|
|||
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
|
||||
import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector, Zoning}
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo}
|
||||
import net.psforever.persistence
|
||||
import net.psforever.types._
|
||||
import org.log4s.MDC
|
||||
import scodec.bits.ByteVector
|
||||
import net.psforever.services.ServiceManager.LookupResult
|
||||
import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
|
||||
import net.psforever.services.chat.ChatService
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse}
|
||||
import net.psforever.services.local.support.RouterTelepadActivation
|
||||
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
|
||||
|
|
@ -84,20 +75,8 @@ import net.psforever.services.teamwork.{
|
|||
}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
|
||||
import net.psforever.services.{InterstellarClusterService, RemoverActor, Service, ServiceManager}
|
||||
import net.psforever.login.{DropCryptoSession, DropSession, HelloFriend, RawPacket}
|
||||
import net.psforever.types._
|
||||
import net.psforever.util.{Config, DefinitionUtil}
|
||||
import net.psforever.login.WorldSession._
|
||||
import net.psforever.zones.Zones
|
||||
import net.psforever.services.chat.ChatService
|
||||
import net.psforever.objects.avatar.Cosmetic
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.util.Success
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import scala.collection.mutable
|
||||
|
||||
object SessionActor {
|
||||
|
||||
|
|
@ -1301,49 +1280,6 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
taskResolver ! RegisterNewAvatar(player)
|
||||
}
|
||||
|
||||
case msg @ Zoning.InstantAction.Located(zone, _, spawn_point) =>
|
||||
//in between subsequent reply messages, it does not matter if the destination changes
|
||||
//so long as there is at least one destination at all (including the fallback)
|
||||
if (ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction), cluster)) {
|
||||
val (pos, ori) = spawn_point.SpecificPoint(player)
|
||||
SpawnThroughZoningProcess(zone, pos, ori)
|
||||
} else if (zoningStatus != Zoning.Status.None) {
|
||||
instantActionFallbackDestination = Some(msg)
|
||||
}
|
||||
|
||||
case Zoning.InstantAction.NotLocated() =>
|
||||
instantActionFallbackDestination match {
|
||||
case Some(Zoning.InstantAction.Located(zone, _, spawn_point))
|
||||
if spawn_point.Owner.Faction == player.Faction && !spawn_point.Offline =>
|
||||
if (ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction), cluster)) {
|
||||
val (pos, ori) = spawn_point.SpecificPoint(player)
|
||||
SpawnThroughZoningProcess(zone, pos, ori)
|
||||
} else if (zoningCounter == 0) {
|
||||
CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable")
|
||||
}
|
||||
case _ =>
|
||||
//no instant action available
|
||||
CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable")
|
||||
}
|
||||
|
||||
case Zoning.Recall.Located(zone, spawn_point) =>
|
||||
if (ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id), cluster)) {
|
||||
val (pos, ori) = spawn_point.SpecificPoint(player)
|
||||
SpawnThroughZoningProcess(zone, pos, ori)
|
||||
}
|
||||
|
||||
case Zoning.Recall.Denied(reason) =>
|
||||
CancelZoningProcessWithReason(s"@norecall_sanctuary_$reason", Some(ChatMessageType.CMT_QUIT))
|
||||
|
||||
case Zoning.Quit() =>
|
||||
if (ContemplateZoningResponse(Zoning.Quit(), self)) {
|
||||
log.info("Good-bye")
|
||||
ImmediateDisconnect()
|
||||
}
|
||||
|
||||
case ZoningReset() =>
|
||||
CancelZoningProcess()
|
||||
|
||||
case NewPlayerLoaded(tplayer) =>
|
||||
//new zone
|
||||
log.info(s"Player ${tplayer.Name} has been loaded")
|
||||
|
|
@ -3792,7 +3728,7 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
}
|
||||
}
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.Id,
|
||||
continent.id,
|
||||
VehicleAction.UpdateAmsSpawnPoint(continent)
|
||||
)
|
||||
upstreamMessageCount = 0
|
||||
|
|
@ -7753,7 +7689,8 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
val outProjectile = if(projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)) {
|
||||
val quality = projectile.profile.Aggravated match {
|
||||
case Some(aggravation)
|
||||
if aggravation.targets.exists(validation => validation.test(target)) =>
|
||||
if aggravation.targets.exists(validation => validation.test(target)) &&
|
||||
aggravation.info.exists(_.damage_type == AggravatedDamage.basicDamageType(resolution)) =>
|
||||
ProjectileQuality.AggravatesTarget
|
||||
case _ =>
|
||||
ProjectileQuality.Normal
|
||||
|
|
@ -8013,12 +7950,12 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
* @return `true`, if the desired certification requirements are met; `false`, otherwise
|
||||
*/
|
||||
def ConstructionItemPermissionComparison(
|
||||
sample: Set[CertificationType.Value],
|
||||
test: Set[CertificationType.Value]
|
||||
sample: Set[Certification],
|
||||
test: Set[Certification]
|
||||
): Boolean = {
|
||||
import CertificationType._
|
||||
val engineeringCerts: Set[CertificationType.Value] = Set(AssaultEngineering, FortificationEngineering)
|
||||
val testDiff: Set[CertificationType.Value] = test diff (engineeringCerts ++ Set(AdvancedEngineering))
|
||||
import Certification._
|
||||
val engineeringCerts: Set[Certification] = Set(AssaultEngineering, FortificationEngineering)
|
||||
val testDiff: Set[Certification] = test diff (engineeringCerts ++ Set(AdvancedEngineering))
|
||||
//substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering`
|
||||
val sampleIntersect = if (sample contains AdvancedEngineering) {
|
||||
engineeringCerts
|
||||
|
|
@ -8602,7 +8539,7 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
) {
|
||||
//do not delete if vehicle has passengers or cargo
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.Id,
|
||||
continent.id,
|
||||
VehicleAction.UnloadVehicle(pguid, continent, vehicle, topLevel)
|
||||
)
|
||||
None
|
||||
|
|
@ -9438,7 +9375,7 @@ class SessionActor extends Actor with MDCContextAware {
|
|||
)
|
||||
taskResolver ! (if (projectile.HasGUID) {
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.Id,
|
||||
continent.id,
|
||||
AvatarAction.ProjectileExplodes(
|
||||
player.GUID,
|
||||
projectile.GUID,
|
||||
|
|
|
|||
|
|
@ -3918,7 +3918,11 @@ object GlobalDefinitions {
|
|||
2000,
|
||||
0f,
|
||||
true,
|
||||
List(TargetValidation(EffectTarget.Category.Aircraft, EffectTarget.Validation.Aircraft))
|
||||
List(
|
||||
TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player),
|
||||
TargetValidation(EffectTarget.Category.Vehicle, EffectTarget.Validation.Vehicle),
|
||||
TargetValidation(EffectTarget.Category.Turret, EffectTarget.Validation.Turret)
|
||||
)
|
||||
)
|
||||
starfire_projectile.InitialVelocity = 45
|
||||
starfire_projectile.Lifespan = 7.8f
|
||||
|
|
|
|||
|
|
@ -16,33 +16,68 @@ class ProjectileDefinition(objectId: Int)
|
|||
with JammingUnit
|
||||
with StandardDamageProfile
|
||||
with DamageModifiers {
|
||||
/** ascertain that this object is a valid projectile type */
|
||||
private val projectileType: Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException
|
||||
/** how much faster (or slower) the projectile moves (m/s^2^) */
|
||||
private var acceleration: Int = 0
|
||||
/** when the acceleration stops being applied (s) */
|
||||
private var accelerationUntil: Float = 0f
|
||||
/** the type of damage that the projectile causes */
|
||||
private var damageType: DamageType.Value = DamageType.None
|
||||
/** an auxillary type of damage that the projectile causes */
|
||||
private var damageTypeSecondary: DamageType.Value = DamageType.None
|
||||
/** against Infantry targets, this projectile does not do armor damage */
|
||||
private var damageToHealthOnly: Boolean = false
|
||||
/** number of seconds before an airborne projectile's damage begins to degrade (s) */
|
||||
private var degradeDelay: Float = 1f
|
||||
/** the rate of degrade of projectile damage after the degrade delay */
|
||||
private var degradeMultiplier: Float = 1f
|
||||
/** the out-of-the-muzzle speed of a projectile (m/s) */
|
||||
private var initialVelocity: Int = 1
|
||||
/** for how long the projectile exists (s) */
|
||||
private var lifespan: Float = 1f
|
||||
/** for radial damage, how much damage has been lost the further away from the impact point (m) */
|
||||
private var damageAtEdge: Float = 1f
|
||||
/** for radial damage, the radial distance of the explosion effect (m) */
|
||||
private var damageRadius: Float = 1f
|
||||
/** for lashing damage, how far away a target will be affected by the projectile (m) */
|
||||
private var lashRadius : Float = 0f
|
||||
/** use a specific modifier as a part of damage calculations */
|
||||
private var useDamage1Subtract: Boolean = false
|
||||
private var existsOnRemoteClients: Boolean = false //`true` spawns a server-managed object
|
||||
private var remoteClientData: (Int, Int) =
|
||||
(0, 0) //artificial values; for ObjectCreateMessage packet (oicw_little_buddy is undefined)
|
||||
/** the projectile is represented by a server-side entity
|
||||
* that is updated by the projectile owner
|
||||
* and transmitted to all projectile observers;
|
||||
* `true` spawns a server-managed object */
|
||||
private var existsOnRemoteClients: Boolean = false
|
||||
/** the values used by the `ObjectCreateMessage` packet for construction of the server-managed projectile
|
||||
* `0, 0` are artificial values;
|
||||
* the oicw_little_buddy is undefined for these values */
|
||||
private var remoteClientData: (Int, Int) = (0, 0)
|
||||
/** some other entity confers projectile damage;
|
||||
* a set value should not `None` and not `0` but is preferred to be the damager's uid */
|
||||
private var damageProxy: Option[Int] = None
|
||||
/** this projectile follows its target, after a fashion */
|
||||
private var autoLock: Boolean = false
|
||||
/** na;
|
||||
* currently used with jammer properties only */
|
||||
private var additionalEffect: Boolean = false
|
||||
/** the projectile tries to confer the jammered status effect to its target(s) */
|
||||
private var jammerProjectile: Boolean = false
|
||||
/** projectile takes the form of a type of "grenade";
|
||||
* grenades arc with gravity rather than travel in a relatively straight path */
|
||||
private var grenade_projectile : Boolean = false
|
||||
/** projectile tries to confers aggravated damage burn to its target */
|
||||
private var aggravated_damage : Option[AggravatedDamage] = None
|
||||
//derived calculations
|
||||
/** the calculated distance at which the projectile have traveled far enough to despawn (m);
|
||||
* typically handled as the projectile no longer performing damage;
|
||||
* occasionally, this value is purely mathematical as opposed to realistic, e.g., the melee weapons */
|
||||
private var distanceMax: Float = 0f
|
||||
/** how far the projectile will travel while accelerating (m) */
|
||||
private var distanceFromAcceleration: Float = 0f
|
||||
/** how far the projectile will travel while no degrading (m) */
|
||||
private var distanceNoDegrade: Float = 0f
|
||||
/** after acceleration, if any, what is the final speed of the projectile (m/s) */
|
||||
private var finalVelocity: Float = 0f
|
||||
Name = "projectile"
|
||||
Modifiers = DamageModifiers.DistanceDegrade
|
||||
|
|
|
|||
|
|
@ -97,14 +97,11 @@ object DamageModifiers {
|
|||
def Calculate: DamageModifiers.Format = function
|
||||
|
||||
private def function(damage: Int, data: ResolvedProjectile): Int = {
|
||||
val projectile = data.projectile
|
||||
val profile = projectile.profile
|
||||
val profile = data.projectile.profile
|
||||
val distance = Vector3.Distance(data.hit_pos, data.target.Position)
|
||||
val radius = profile.DamageRadius
|
||||
if (distance <= radius) {
|
||||
val base: Float = profile.DamageAtEdge
|
||||
val degrade: Float = (1 - base) * ((radius - distance) / radius) + base
|
||||
(damage * degrade).toInt
|
||||
damage * (1f - (profile.DamageAtEdge * distance / radius)).toInt
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue