2020-12-13 15:01:13 +00:00
|
|
|
//=============================================================================
|
|
|
|
// KFProj_BallisticExplosive
|
|
|
|
//=============================================================================
|
|
|
|
// Superclass for projectiles that need to fly through the air quickly
|
|
|
|
// and damage things they impact, but also explode
|
|
|
|
//=============================================================================
|
|
|
|
// Killing Floor 2
|
|
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
|
|
// - John "Ramm-Jaeger" Gibso
|
|
|
|
//=============================================================================
|
|
|
|
|
|
|
|
class KFProj_BallisticExplosive extends KFProjectile
|
|
|
|
abstract
|
|
|
|
hidedropdown;
|
|
|
|
|
|
|
|
/** This projectile is a dud, it hit too soon */
|
|
|
|
var repnotify bool bDud;
|
|
|
|
|
|
|
|
/** When true this projectile will collide/hit/explode when hitting teammates */
|
|
|
|
var() bool bCollideWithTeammates;
|
|
|
|
|
|
|
|
var bool bClientDudHit;
|
|
|
|
|
|
|
|
var repnotify bool bIsTimedExplosive;
|
|
|
|
|
|
|
|
/** Distance this projectile arms itself at */
|
|
|
|
var() float ArmDistSquared;
|
|
|
|
|
|
|
|
/** The effect to display when the projectile becomes a dud */
|
|
|
|
var(Projectile) ParticleSystem ProjDudTemplate;
|
|
|
|
|
|
|
|
/** Information about grenade bounce effects for this class */
|
|
|
|
var KFImpactEffectInfo GrenadeBounceEffectInfo;
|
|
|
|
|
|
|
|
/** Dampen amount for every bounce */
|
|
|
|
var() float DampenFactor;
|
|
|
|
|
|
|
|
/** Dampen amount for parallel angle to velocity */
|
|
|
|
var() float DampenFactorParallel;
|
|
|
|
|
|
|
|
/** Dampen amount for every bounce */
|
|
|
|
var() float WallHitDampenFactor;
|
|
|
|
|
|
|
|
/** Dampen amount for parallel angle to velocity */
|
|
|
|
var() float WallHitDampenFactorParallel;
|
|
|
|
|
|
|
|
/** A smaller speed will cause the projectile to stop */
|
|
|
|
var() float MinSpeedBeforeStop;
|
|
|
|
|
|
|
|
/** Set to true to reset the projectile rotation when it stops because of low speed */
|
|
|
|
var() bool ResetRotationOnStop;
|
|
|
|
|
|
|
|
/** How much to offset the emitter mesh when the grenade has landed so that it doesn't penetrate the ground */
|
|
|
|
var() vector LandedTranslationOffset;
|
|
|
|
|
|
|
|
//to keep track if we are hitting an actor multiple times.
|
|
|
|
var array<Actor> ImpactList;
|
|
|
|
|
2022-11-27 21:49:25 +00:00
|
|
|
var() bool bCanApplyDemolitionistPerks;
|
|
|
|
|
2020-12-13 15:01:13 +00:00
|
|
|
replication
|
|
|
|
{
|
|
|
|
if ( Role == ROLE_Authority && !bNetOwner )
|
|
|
|
bDud;
|
|
|
|
|
|
|
|
if( bNetInitial && !bNetOwner )
|
|
|
|
ArmDistSquared;
|
|
|
|
|
|
|
|
if (bNetDirty)
|
|
|
|
bIsTimedExplosive;
|
|
|
|
}
|
|
|
|
|
2022-12-07 20:25:49 +00:00
|
|
|
simulated function bool CanDud()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-12-13 15:01:13 +00:00
|
|
|
simulated function SyncOriginalLocation()
|
|
|
|
{
|
|
|
|
local KFPerk InstigatorPerk;
|
|
|
|
local KFPawn KFP;
|
|
|
|
|
|
|
|
KFP = KFPawn(Instigator);
|
|
|
|
|
|
|
|
if( KFP != none )
|
|
|
|
{
|
2022-12-07 20:25:49 +00:00
|
|
|
if (CanDud())
|
2020-12-13 15:01:13 +00:00
|
|
|
{
|
2022-12-07 20:25:49 +00:00
|
|
|
InstigatorPerk = KFP.GetPerk();
|
|
|
|
if( InstigatorPerk != none && InstigatorPerk.ShouldNeverDud() )
|
|
|
|
{
|
|
|
|
ArmDistSquared = 0;
|
|
|
|
}
|
2020-12-13 15:01:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If server has already collided we need to recover our velocity. Reuse the code
|
|
|
|
// in super by zeroing velocity.
|
|
|
|
if ( Physics == PHYS_Falling && Role < ROLE_Authority
|
|
|
|
&& Instigator != none && Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
Velocity = vect(0,0,0);
|
|
|
|
}
|
|
|
|
|
|
|
|
Super.SyncOriginalLocation();
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated event PreBeginPlay()
|
|
|
|
{
|
|
|
|
Super.PreBeginPlay();
|
|
|
|
AdjustCanDisintigrate();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is only here to handle Projectile class calls
|
|
|
|
* Should use TriggerExplosion instead which handles delayed shutdown
|
|
|
|
*/
|
|
|
|
simulated function Explode(vector HitLocation, vector HitNormal)
|
|
|
|
{
|
|
|
|
// Just do a standard explosion if its a locally authoritive player
|
|
|
|
if( WorldInfo.NetMode == NM_Standalone ||
|
|
|
|
(WorldInfo.NetMode == NM_ListenServer && Instigator != none && Instigator.IsLocallyControlled()) )
|
|
|
|
{
|
|
|
|
Super.Explode(HitLocation, HitNormal);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replication the explosion to the server if this is the owning client
|
|
|
|
if( Owner != none && KFWeapon( Owner ) != none && Instigator != none )
|
|
|
|
{
|
|
|
|
if( Instigator.Role < ROLE_Authority && Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
KFWeapon(Owner).HandleClientProjectileExplosion(HitLocation, self);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
StopSimulating();
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Pre-process calling the Explode function so it can be overridden in subclasses */
|
|
|
|
simulated function CallExplode( vector HitLocation, vector HitNormal )
|
|
|
|
{
|
|
|
|
// Move this projectile to the explode location since it might have been
|
|
|
|
// replicated, and this ensures it happens in the right place
|
|
|
|
SetLocation( HitLocation );
|
|
|
|
Super.Explode(HitLocation, HitNormal);
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated event HitWall(vector HitNormal, actor Wall, PrimitiveComponent WallComp)
|
|
|
|
{
|
|
|
|
local Vector VNorm;
|
|
|
|
local rotator NewRotation;
|
|
|
|
local Vector Offset;
|
|
|
|
local bool bWantsClientSideDudHit;
|
|
|
|
local TraceHitInfo HitInfo;
|
|
|
|
local float TraveledDistance;
|
|
|
|
|
|
|
|
// Need to do client side dud hits if this is a client
|
|
|
|
if( Instigator != none && Instigator.Role < ROLE_Authority )
|
|
|
|
{
|
|
|
|
bWantsClientSideDudHit = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
TraveledDistance = (`TimeSince(CreationTime) * Speed);
|
|
|
|
TraveledDistance *= TraveledDistance;
|
|
|
|
|
|
|
|
// Bounce off the wall and cause the shell to dud if we hit too close
|
|
|
|
if( bDud || ((TraveledDistance < ArmDistSquared) || bIsTimedExplosive || (OriginalLocation == vect(0,0,0) && ArmDistSquared > 0)))
|
|
|
|
{
|
|
|
|
// Reflect off Wall w/damping
|
|
|
|
VNorm = (Velocity dot HitNormal) * HitNormal;
|
|
|
|
Velocity = -VNorm * WallHitDampenFactor + (Velocity - VNorm) * WallHitDampenFactorParallel;
|
|
|
|
Speed = VSize(Velocity);
|
|
|
|
|
|
|
|
if( (!bDud || ( bWantsClientSideDudHit && !bClientDudHit)) )
|
|
|
|
{
|
|
|
|
SetIsDud(bWantsClientSideDudHit, HitNormal);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( WorldInfo.NetMode != NM_DedicatedServer && Pawn(Wall) == none )
|
|
|
|
{
|
|
|
|
// do the impact effects
|
|
|
|
`ImpactEffectManager.PlayImpactEffects(Location, Instigator, HitNormal, GrenadeBounceEffectInfo, true );
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we hit a pawn or we are moving too slowly stop moving and lay down flat
|
|
|
|
if ( Speed < MinSpeedBeforeStop )
|
|
|
|
{
|
|
|
|
ImpactedActor = Wall;
|
|
|
|
SetPhysics(PHYS_None);
|
|
|
|
|
|
|
|
if( ProjEffects != none )
|
|
|
|
{
|
|
|
|
ProjEffects.SetTranslation(LandedTranslationOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Position the shell on the ground
|
|
|
|
RotationRate.Yaw = 0;
|
|
|
|
RotationRate.Pitch = 0;
|
|
|
|
RotationRate.Roll = 0;
|
|
|
|
NewRotation = Rotation;
|
|
|
|
NewRotation.Pitch = 0;
|
|
|
|
if(ResetRotationOnStop)
|
|
|
|
{
|
|
|
|
SetRotation(NewRotation);
|
|
|
|
}
|
|
|
|
Offset.Z = LandedTranslationOffset.X;
|
|
|
|
SetLocation(Location + Offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !Wall.bStatic && Wall.bCanBeDamaged && (DamageRadius == 0 || bDamageDestructiblesOnTouch) && !CheckRepeatingTouch(Wall) )
|
|
|
|
{
|
|
|
|
HitInfo.HitComponent = WallComp;
|
|
|
|
HitInfo.Item = INDEX_None;
|
|
|
|
Wall.TakeDamage( Damage, InstigatorController, Location, MomentumTransfer * Normal(Velocity), MyDamageType, HitInfo, self);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !bDud && !bIsTimedExplosive )
|
|
|
|
{
|
|
|
|
Super.HitWall(HitNormal, Wall, WallComp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** returns terminal velocity (max speed while falling) for this actor. Unless overridden, it returns the TerminalVelocity of the PhysicsVolume in which this actor is located.
|
|
|
|
* Overridden so we can have projectiles falling faster than the PhysicsVolume terminal velocity
|
|
|
|
*/
|
|
|
|
function float GetTerminalVelocity()
|
|
|
|
{
|
|
|
|
if( bDud )
|
|
|
|
{
|
|
|
|
// Use the actor super class terminal velocity so duds fall slowly, but
|
|
|
|
// the projectile can still move quickly when it is flying through the air
|
|
|
|
return Super(Actor).GetTerminalVelocity();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return Super.GetTerminalVelocity();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============
|
|
|
|
// Touching
|
|
|
|
simulated function ProcessTouch(Actor Other, Vector HitLocation, Vector HitNormal)
|
|
|
|
{
|
|
|
|
local bool bWantsClientSideDudHit;
|
|
|
|
local float TraveledDistance;
|
|
|
|
local Vector VNorm;
|
|
|
|
|
|
|
|
// If we collided with a Siren shield, let the shield code handle touches
|
|
|
|
if( Other.IsA('KFTrigger_SirenProjectileShield') )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !bCollideWithTeammates && Pawn(Other) != None )
|
|
|
|
{
|
|
|
|
// Don't hit teammates
|
|
|
|
if( Other.GetTeamNum() == GetTeamNum() )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Need to do client side dud hits if this is a client
|
|
|
|
if( Instigator != none && Instigator.Role < ROLE_Authority )
|
|
|
|
{
|
|
|
|
bWantsClientSideDudHit = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
TraveledDistance = (`TimeSince(CreationTime) * Speed);
|
|
|
|
TraveledDistance *= TraveledDistance;
|
|
|
|
|
|
|
|
if( (!bDud || ( bWantsClientSideDudHit && !bClientDudHit)) && ((TraveledDistance < ArmDistSquared) || bIsTimedExplosive || (OriginalLocation == vect(0,0,0) && ArmDistSquared > 0)))
|
|
|
|
{
|
|
|
|
// Don't touch the same actor multiple time's immediately after just
|
|
|
|
// touching it if the TouchTimeThreshhold is set to greater than 0.
|
|
|
|
// This was causing projectiles just to "stop" sometimes when hitting
|
|
|
|
// dead/ragdolled pawns because it was counting as multiple penetrations
|
|
|
|
if( LastTouched.Actor == Other && TouchTimeThreshhold > 0
|
|
|
|
&& `TimeSince(LastTouched.Time) <= TouchTimeThreshhold )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//TODO: Add an impact sound here
|
|
|
|
SetIsDud(bWantsClientSideDudHit, HitNormal);
|
|
|
|
|
|
|
|
if (Other != Instigator && !Other.bStatic && Other.GetTeamNum() != GetTeamNum() && !CheckRepeatingTouch(Other))
|
|
|
|
{
|
|
|
|
ProcessBulletTouch(Other, HitLocation, HitNormal);
|
|
|
|
}
|
|
|
|
// Reflect off Wall w/damping
|
|
|
|
VNorm = (Velocity dot HitNormal) * HitNormal;
|
|
|
|
Velocity = -VNorm * DampenFactor + (Velocity - VNorm) * DampenFactorParallel;
|
|
|
|
Speed = VSize(Velocity);
|
|
|
|
}
|
|
|
|
if (!bDud && !bIsTimedExplosive)
|
|
|
|
{
|
|
|
|
// Process impact hits
|
|
|
|
if (Other != Instigator && !Other.bStatic)
|
|
|
|
{
|
|
|
|
// check/ignore repeat touch events
|
|
|
|
if( !CheckRepeatingTouch(Other) && Other.GetTeamNum() != GetTeamNum())
|
|
|
|
{
|
|
|
|
ProcessBulletTouch(Other, HitLocation, HitNormal);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( WorldInfo.NetMode == NM_Standalone ||
|
|
|
|
(WorldInfo.NetMode == NM_ListenServer && Instigator != none && Instigator.IsLocallyControlled()) )
|
|
|
|
{
|
|
|
|
Super.ProcessTouch( Other, HitLocation, HitNormal );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( Owner != none && KFWeapon( Owner ) != none && Instigator != none )
|
|
|
|
{
|
|
|
|
if( Instigator.Role < ROLE_Authority && Instigator.IsLocallyControlled() )
|
|
|
|
{
|
|
|
|
KFWeapon(Owner).HandleClientProjectileExplosion(HitLocation, self);
|
|
|
|
Super.ProcessTouch( Other, HitLocation, HitNormal );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
StopSimulating();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Handle bullet collision and damage */
|
|
|
|
simulated function ProcessBulletTouch(Actor Other, Vector HitLocation, Vector HitNormal)
|
|
|
|
{
|
|
|
|
if (ImpactList.Find(Other) != INDEX_NONE)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (Other != none)
|
|
|
|
{
|
|
|
|
ImpactList.AddItem(Other);
|
|
|
|
}
|
|
|
|
|
|
|
|
super.ProcessBulletTouch(Other, HitLocation, HitNormal);
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated function SetIsDud(bool bWantsClientSideDudHit, vector HitNormal)
|
|
|
|
{
|
|
|
|
bDud = true;
|
|
|
|
|
|
|
|
if (bWantsClientSideDudHit)
|
|
|
|
{
|
|
|
|
bClientDudHit = true;
|
|
|
|
}
|
|
|
|
LifeSpan = 1.0;
|
|
|
|
SetPhysics(PHYS_Falling);
|
|
|
|
GravityScale = 1.0;
|
|
|
|
|
|
|
|
// Replace the flying effects with the dud mesh effects
|
|
|
|
StopFlightEffects();
|
|
|
|
ProjFlightTemplate = ProjDudTemplate;
|
|
|
|
ProjFlightTemplateZedTime = ProjDudTemplate;
|
|
|
|
SpawnFlightEffects();
|
|
|
|
OnDudEffect();
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated function OnDudEffect();
|
|
|
|
|
|
|
|
/** Returns a list of hitzone impacts for a collision with a given pawn
|
|
|
|
* @note: To trace the PhysicsAsset the pawn cylinder should have BlockZeroExtent=FALSE
|
|
|
|
*/
|
|
|
|
simulated function bool TraceProjHitZones(Pawn P, vector EndTrace, vector StartTrace, out array<ImpactInfo> out_Hits)
|
|
|
|
{
|
|
|
|
local vector ClosestHit, Direction;
|
|
|
|
|
|
|
|
Super.TraceProjHitZones(P, EndTrace, StartTrace, out_Hits);
|
|
|
|
|
|
|
|
// If we didn't hit anything, that means we hit the big cylinder but
|
|
|
|
// missed the body. Just grab the nearest bone and hit that
|
|
|
|
if( bDud && out_Hits.length == 0 )
|
|
|
|
{
|
|
|
|
P.Mesh.FindClosestBone(StartTrace, ClosestHit);
|
|
|
|
|
|
|
|
if( !IsZero(ClosestHit) )
|
|
|
|
{
|
|
|
|
StartTrace = ClosestHit;
|
|
|
|
Direction = Normal(ClosestHit-StartTrace);
|
|
|
|
EndTrace = StartTrace + Direction * (P.CylinderComponent.CollisionRadius * 6.0);
|
|
|
|
|
|
|
|
Super.TraceProjHitZones(P, EndTrace, StartTrace, out_Hits);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out_Hits.Length > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// for nukes && concussive force
|
|
|
|
simulated protected function PrepareExplosionTemplate()
|
|
|
|
{
|
2022-11-27 21:49:25 +00:00
|
|
|
if (bCanApplyDemolitionistPerks)
|
|
|
|
{
|
|
|
|
class'KFPerk_Demolitionist'.static.PrepareExplosive( Instigator, self );
|
|
|
|
}
|
2020-12-13 15:01:13 +00:00
|
|
|
|
|
|
|
super.PrepareExplosionTemplate();
|
|
|
|
}
|
|
|
|
|
|
|
|
simulated protected function SetExplosionActorClass()
|
|
|
|
{
|
|
|
|
local KFPlayerReplicationInfo InstigatorPRI;
|
|
|
|
|
|
|
|
if( bWasTimeDilated && Instigator != none )
|
|
|
|
{
|
|
|
|
InstigatorPRI = KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo);
|
|
|
|
if( InstigatorPRI != none )
|
|
|
|
{
|
|
|
|
if( InstigatorPRI.bNukeActive && class'KFPerk_Demolitionist'.static.ProjectileShouldNuke( self ) )
|
|
|
|
{
|
|
|
|
ExplosionActorClass = class'KFPerk_Demolitionist'.static.GetNukeExplosionActorClass();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
super.SetExplosionActorClass();
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
bBounce=true
|
|
|
|
ProjEffectsFadeOutDuration=0.0
|
|
|
|
|
|
|
|
DampenFactor=0.025000
|
|
|
|
DampenFactorParallel=0.050000
|
|
|
|
WallHitDampenFactor=0.025000
|
|
|
|
WallHitDampenFactorParallel=0.050000
|
|
|
|
MinSpeedBeforeStop=40
|
|
|
|
ResetRotationOnStop=true
|
|
|
|
LandedTranslationOffset=(X=2)
|
|
|
|
|
|
|
|
bCollideComplex=TRUE // Ignore simple collision on StaticMeshes, and collide per poly
|
|
|
|
|
|
|
|
// CollideActors=true allows detection via OverlappingActors or CollidingActors (for Siren scream)
|
|
|
|
Begin Object Name=CollisionCylinder
|
|
|
|
CollisionRadius=5.f
|
|
|
|
CollisionHeight=5.f
|
|
|
|
BlockNonZeroExtent=false
|
|
|
|
CollideActors=true
|
|
|
|
End Object
|
|
|
|
|
|
|
|
// Lets the projectile collide with both the big collision cylinder, and any
|
|
|
|
// parts of the pawn outside the collision cylinder
|
|
|
|
ExtraLineCollisionOffsets.Add(())
|
|
|
|
|
|
|
|
bNetTemporary=False
|
|
|
|
bBlockedByInstigator=false
|
|
|
|
bAlwaysReplicateExplosion=true
|
|
|
|
bUseClientSideHitDetection=true
|
|
|
|
bNoReplicationToInstigator=false
|
|
|
|
bReplicateLocationOnExplosion=true
|
|
|
|
bValidateExplosionNormalOnClient=true
|
|
|
|
|
|
|
|
bSyncToOriginalLocation=true
|
|
|
|
bSyncToThirdPersonMuzzleLocation=true
|
|
|
|
|
|
|
|
ExplosionActorClass=class'KFExplosionActor'
|
|
|
|
|
|
|
|
TouchTimeThreshhold=0.15
|
|
|
|
|
|
|
|
Begin Object Class=AkComponent name=AmbientAkSoundComponent
|
|
|
|
bStopWhenOwnerDestroyed=true
|
|
|
|
bForceOcclusionUpdateInterval=true
|
|
|
|
OcclusionUpdateInterval=0.25;
|
|
|
|
End Object
|
|
|
|
AmbientComponent=AmbientAkSoundComponent
|
|
|
|
Components.Add(AmbientAkSoundComponent)
|
|
|
|
|
|
|
|
bAutoStartAmbientSound=true
|
|
|
|
bStopAmbientSoundOnExplode=true
|
|
|
|
|
|
|
|
bCanDisintegrate=true
|
|
|
|
GlassShatterType=FMGS_ShatterAll
|
|
|
|
// network
|
|
|
|
bAlwaysReplicateDisintegration=true
|
|
|
|
AlwaysRelevantDistanceSquared=6250000 // 25m
|
|
|
|
|
|
|
|
bCollideWithTeammates=false
|
|
|
|
|
|
|
|
bDamageDestructiblesOnTouch=true
|
2022-11-27 21:49:25 +00:00
|
|
|
|
|
|
|
bCanApplyDemolitionistPerks=true
|
2020-12-13 15:01:13 +00:00
|
|
|
}
|
|
|
|
|