mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-19 06:28:40 +03:00
Lots of stuff. Also moved everything I could to the _Shitmed namespace
as I do in Goob. Will make future ports way faster
# Changelog
🆑 Mocho
- add: Added some fun organs and other thingies, check out the Goob PRs
if you want more details.
- fix: Fixed tons of issues with shitmed. Too many for the changelog in
fact.
(cherry picked from commit 3c9db94102cb25b28a83d51ac8d659fa31fe7d12)
520 lines
20 KiB
C#
520 lines
20 KiB
C#
using Content.Shared.Body.Components;
|
|
using Content.Shared.Body.Part;
|
|
using Content.Shared._Shitmed.Body.Events;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Damage.Prototypes;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Steps.Parts;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Standing;
|
|
using Content.Shared._Shitmed.Targeting;
|
|
using Content.Shared._Shitmed.Targeting.Events;
|
|
using Robust.Shared.CPUJob.JobQueues;
|
|
using Robust.Shared.CPUJob.JobQueues.Queues;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Content.Shared.Inventory;
|
|
|
|
// Namespace has set accessors, leaving it on the default.
|
|
namespace Content.Shared.Body.Systems;
|
|
|
|
public partial class SharedBodySystem
|
|
{
|
|
[Dependency] private readonly INetManager _net = default!;
|
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly DamageableSystem _damageable = default!;
|
|
[Dependency] private readonly StandingStateSystem _standing = default!;
|
|
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
private readonly string[] _severingDamageTypes = { "Slash", "Piercing", "Blunt" };
|
|
private const double IntegrityJobTime = 0.005;
|
|
private readonly JobQueue _integrityJobQueue = new(IntegrityJobTime);
|
|
public sealed class IntegrityJob : Job<object>
|
|
{
|
|
private readonly SharedBodySystem _self;
|
|
private readonly Entity<BodyPartComponent> _ent;
|
|
public IntegrityJob(SharedBodySystem self, Entity<BodyPartComponent> ent, double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation)
|
|
{
|
|
_self = self;
|
|
_ent = ent;
|
|
}
|
|
|
|
public IntegrityJob(SharedBodySystem self, Entity<BodyPartComponent> ent, double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) : base(maxTime, stopwatch, cancellation)
|
|
{
|
|
_self = self;
|
|
_ent = ent;
|
|
}
|
|
|
|
protected override Task<object?> Process()
|
|
{
|
|
_self.ProcessIntegrityTick(_ent);
|
|
|
|
return Task.FromResult<object?>(null);
|
|
}
|
|
}
|
|
|
|
private EntityQuery<TargetingComponent> _queryTargeting;
|
|
private void InitializeIntegrityQueue()
|
|
{
|
|
_queryTargeting = GetEntityQuery<TargetingComponent>();
|
|
SubscribeLocalEvent<BodyComponent, TryChangePartDamageEvent>(OnTryChangePartDamage);
|
|
SubscribeLocalEvent<BodyComponent, DamageModifyEvent>(OnBodyDamageModify);
|
|
SubscribeLocalEvent<BodyPartComponent, DamageModifyEvent>(OnPartDamageModify);
|
|
SubscribeLocalEvent<BodyPartComponent, DamageChangedEvent>(OnDamageChanged);
|
|
}
|
|
|
|
private void ProcessIntegrityTick(Entity<BodyPartComponent> entity)
|
|
{
|
|
if (!TryComp<DamageableComponent>(entity, out var damageable))
|
|
return;
|
|
|
|
var damage = damageable.TotalDamage;
|
|
|
|
if (entity.Comp is { Body: { } body }
|
|
&& damage > entity.Comp.MinIntegrity
|
|
&& damage <= entity.Comp.IntegrityThresholds[TargetIntegrity.HeavilyWounded]
|
|
&& _queryTargeting.HasComp(body)
|
|
&& !_mobState.IsDead(body))
|
|
_damageable.TryChangeDamage(entity, GetHealingSpecifier(entity), canSever: false, targetPart: GetTargetBodyPart(entity));
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
//UpdateOrgan(frameTime);
|
|
_integrityJobQueue.Process();
|
|
|
|
if (!_timing.IsFirstTimePredicted)
|
|
return;
|
|
|
|
using var query = EntityQueryEnumerator<BodyPartComponent>();
|
|
while (query.MoveNext(out var ent, out var part))
|
|
{
|
|
part.HealingTimer += frameTime;
|
|
|
|
if (part.HealingTimer >= part.HealingTime)
|
|
{
|
|
part.HealingTimer = 0;
|
|
_integrityJobQueue.EnqueueJob(new IntegrityJob(this, (ent, part), IntegrityJobTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnTryChangePartDamage(Entity<BodyComponent> ent, ref TryChangePartDamageEvent args)
|
|
{
|
|
// If our target has a TargetingComponent, that means they will take limb damage
|
|
// And if their attacker also has one, then we use that part.
|
|
if (_queryTargeting.TryComp(ent, out var targetEnt))
|
|
{
|
|
var damage = args.Damage;
|
|
TargetBodyPart? targetPart = null;
|
|
|
|
if (args.TargetPart != null)
|
|
{
|
|
targetPart = args.TargetPart;
|
|
}
|
|
else if (args.Origin.HasValue && _queryTargeting.TryComp(args.Origin.Value, out var targeter))
|
|
{
|
|
targetPart = targeter.Target;
|
|
// If the target is Torso then have a 33% chance to hit another part
|
|
if (targetPart.Value == TargetBodyPart.Torso)
|
|
{
|
|
var additionalPart = GetRandomPartSpread(10);
|
|
targetPart = targetPart.Value | additionalPart;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If there's an origin in this case, that means it comes from an entity without TargetingComponent,
|
|
// such as an animal, so we attack a random part.
|
|
if (args.Origin.HasValue)
|
|
{
|
|
// Evasion would trigger constantly if we don't target torso
|
|
targetPart = args.CanEvade ? TargetBodyPart.Torso : GetRandomBodyPart(ent, targetEnt);
|
|
}
|
|
// Otherwise we damage all parts equally (barotrauma, explosions, etc).
|
|
else if (damage != null)
|
|
{
|
|
// Division by 2 cuz damaging all parts by the same damage by default is too much.
|
|
damage /= 2;
|
|
targetPart = TargetBodyPart.All;
|
|
}
|
|
}
|
|
|
|
if (targetPart == null)
|
|
return;
|
|
|
|
if (!TryChangePartDamage(ent, args.Damage, args.IgnoreResistances, args.CanSever, args.CanEvade, args.PartMultiplier, targetPart.Value, out var evaded)
|
|
&& args.CanEvade && evaded)
|
|
{
|
|
if (_net.IsServer)
|
|
_popup.PopupEntity(Loc.GetString("surgery-part-damage-evaded", ("user", Identity.Entity(ent, EntityManager))), ent);
|
|
|
|
args.Evaded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnBodyDamageModify(Entity<BodyComponent> bodyEnt, ref DamageModifyEvent args)
|
|
{
|
|
if (args.TargetPart != null)
|
|
{
|
|
var (targetType, _) = ConvertTargetBodyPart(args.TargetPart.Value);
|
|
args.Damage *= GetPartDamageModifier(targetType);
|
|
}
|
|
}
|
|
|
|
private void OnPartDamageModify(Entity<BodyPartComponent> partEnt, ref DamageModifyEvent args)
|
|
{
|
|
if (partEnt.Comp.Body != null
|
|
&& TryComp(partEnt.Comp.Body.Value, out InventoryComponent? inventory))
|
|
_inventory.RelayEvent((partEnt.Comp.Body.Value, inventory), ref args);
|
|
|
|
if (Prototypes.TryIndex<DamageModifierSetPrototype>("PartDamage", out var partModifierSet))
|
|
args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, partModifierSet);
|
|
|
|
args.Damage *= GetPartDamageModifier(partEnt.Comp.PartType);
|
|
}
|
|
|
|
private bool TryChangePartDamage(EntityUid entity,
|
|
DamageSpecifier damage,
|
|
bool ignoreResistances,
|
|
bool canSever,
|
|
bool canEvade,
|
|
float partMultiplier,
|
|
TargetBodyPart targetParts,
|
|
out bool evaded)
|
|
{
|
|
evaded = false;
|
|
|
|
if (damage.GetTotal() == 0)
|
|
return false;
|
|
|
|
var landed = false;
|
|
var targets = SharedTargetingSystem.GetValidParts();
|
|
foreach (var target in targets)
|
|
{
|
|
if (!targetParts.HasFlag(target))
|
|
continue;
|
|
|
|
var (targetType, targetSymmetry) = ConvertTargetBodyPart(target);
|
|
if (GetBodyChildrenOfType(entity, targetType, symmetry: targetSymmetry) is { } part)
|
|
{
|
|
if (canEvade && TryEvadeDamage(entity, GetEvadeChance(targetType)))
|
|
{
|
|
evaded = true;
|
|
continue;
|
|
}
|
|
|
|
var damageResult = _damageable.TryChangeDamage(part.FirstOrDefault().Id, damage * partMultiplier, ignoreResistances, canSever: canSever);
|
|
if (damageResult != null && damageResult.GetTotal() != 0)
|
|
landed = true;
|
|
}
|
|
}
|
|
|
|
return landed;
|
|
}
|
|
|
|
private void OnDamageChanged(Entity<BodyPartComponent> partEnt, ref DamageChangedEvent args)
|
|
{
|
|
if (!TryComp<DamageableComponent>(partEnt, out var damageable))
|
|
return;
|
|
|
|
var severed = false;
|
|
var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot;
|
|
var delta = args.DamageDelta;
|
|
|
|
if (args.CanSever
|
|
&& partEnt.Comp.CanSever
|
|
&& partIdSlot is not null
|
|
&& delta != null
|
|
&& !HasComp<BodyPartReattachedComponent>(partEnt)
|
|
&& !partEnt.Comp.Enabled
|
|
&& damageable.TotalDamage >= partEnt.Comp.SeverIntegrity
|
|
&& _severingDamageTypes.Any(damageType => delta.DamageDict.TryGetValue(damageType, out var value) && value > 0))
|
|
severed = true;
|
|
|
|
CheckBodyPart(partEnt, GetTargetBodyPart(partEnt), severed, damageable);
|
|
|
|
if (severed)
|
|
DropPart(partEnt);
|
|
|
|
Dirty(partEnt, partEnt.Comp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the random body part rolling a number between 1 and 9, and returns
|
|
/// Torso if the result is 9 or more. The higher torsoWeight is, the higher chance to return it.
|
|
/// By default, the chance to return Torso is 50%.
|
|
/// </summary>
|
|
private TargetBodyPart GetRandomPartSpread(ushort torsoWeight = 9)
|
|
{
|
|
var rand = new System.Random((int) _timing.CurTick.Value);
|
|
|
|
const int targetPartsAmount = 9;
|
|
// 5 = amount of target parts except Torso
|
|
return rand.Next(1, targetPartsAmount + torsoWeight) switch
|
|
{
|
|
1 => TargetBodyPart.Head,
|
|
2 => TargetBodyPart.RightArm,
|
|
3 => TargetBodyPart.RightHand,
|
|
4 => TargetBodyPart.LeftArm,
|
|
5 => TargetBodyPart.LeftHand,
|
|
6 => TargetBodyPart.RightLeg,
|
|
7 => TargetBodyPart.RightFoot,
|
|
8 => TargetBodyPart.LeftLeg,
|
|
9 => TargetBodyPart.LeftFoot,
|
|
_ => TargetBodyPart.Torso,
|
|
};
|
|
}
|
|
|
|
public TargetBodyPart? GetRandomBodyPart(EntityUid uid, TargetingComponent? target = null)
|
|
{
|
|
if (!Resolve(uid, ref target, false))
|
|
return null;
|
|
|
|
var rand = new System.Random((int) _timing.CurTick.Value);
|
|
|
|
var totalWeight = target.TargetOdds.Values.Sum();
|
|
var randomValue = rand.NextFloat() * totalWeight;
|
|
|
|
foreach (var (part, weight) in target.TargetOdds)
|
|
{
|
|
if (randomValue <= weight)
|
|
return part;
|
|
randomValue -= weight;
|
|
}
|
|
|
|
return TargetBodyPart.Torso; // Default to torso if something goes wrong
|
|
}
|
|
|
|
/// <summary>
|
|
/// This should be called after body part damage was changed.
|
|
/// </summary>
|
|
public void CheckBodyPart(
|
|
Entity<BodyPartComponent> partEnt,
|
|
TargetBodyPart? targetPart,
|
|
bool severed,
|
|
DamageableComponent? damageable = null)
|
|
{
|
|
if (!Resolve(partEnt, ref damageable))
|
|
return;
|
|
|
|
var integrity = damageable.TotalDamage;
|
|
|
|
// KILL the body part
|
|
if (partEnt.Comp.Enabled && integrity >= partEnt.Comp.IntegrityThresholds[TargetIntegrity.CriticallyWounded])
|
|
{
|
|
var ev = new BodyPartEnableChangedEvent(false);
|
|
RaiseLocalEvent(partEnt, ref ev);
|
|
}
|
|
|
|
// LIVE the body part
|
|
if (!partEnt.Comp.Enabled && integrity <= partEnt.Comp.IntegrityThresholds[partEnt.Comp.EnableIntegrity] && !severed)
|
|
{
|
|
var ev = new BodyPartEnableChangedEvent(true);
|
|
RaiseLocalEvent(partEnt, ref ev);
|
|
}
|
|
|
|
if (_queryTargeting.TryComp(partEnt.Comp.Body, out var targeting)
|
|
&& HasComp<MobStateComponent>(partEnt.Comp.Body))
|
|
{
|
|
var newIntegrity = GetIntegrityThreshold(partEnt.Comp, integrity.Float(), severed);
|
|
// We need to check if the part is dead to prevent the UI from showing dead parts as alive.
|
|
if (targetPart is not null &&
|
|
targeting.BodyStatus.ContainsKey(targetPart.Value) &&
|
|
targeting.BodyStatus[targetPart.Value] != TargetIntegrity.Dead)
|
|
{
|
|
targeting.BodyStatus[targetPart.Value] = newIntegrity;
|
|
if (targetPart.Value == TargetBodyPart.Torso)
|
|
targeting.BodyStatus[TargetBodyPart.Groin] = newIntegrity;
|
|
|
|
Dirty(partEnt.Comp.Body.Value, targeting);
|
|
}
|
|
// Revival events are handled by the server, so we end up being locked to a network event.
|
|
// I hope you like the _net.IsServer, Remuchi :)
|
|
if (_net.IsServer)
|
|
RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(partEnt.Comp.Body.Value)), partEnt.Comp.Body.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the integrity of all body parts in the entity.
|
|
/// </summary>
|
|
public Dictionary<TargetBodyPart, TargetIntegrity> GetBodyPartStatus(EntityUid entityUid)
|
|
{
|
|
var result = new Dictionary<TargetBodyPart, TargetIntegrity>();
|
|
|
|
if (!TryComp<BodyComponent>(entityUid, out var body))
|
|
return result;
|
|
|
|
foreach (var part in SharedTargetingSystem.GetValidParts())
|
|
{
|
|
result[part] = TargetIntegrity.Severed;
|
|
}
|
|
|
|
foreach (var partComponent in GetBodyChildren(entityUid, body))
|
|
{
|
|
var targetBodyPart = GetTargetBodyPart(partComponent.Component.PartType, partComponent.Component.Symmetry);
|
|
|
|
if (targetBodyPart != null && TryComp<DamageableComponent>(partComponent.Id, out var damageable))
|
|
result[targetBodyPart.Value] = GetIntegrityThreshold(partComponent.Component, damageable.TotalDamage.Float(), false);
|
|
}
|
|
|
|
// Hardcoded shitcode for Groin :)
|
|
result[TargetBodyPart.Groin] = result[TargetBodyPart.Torso];
|
|
|
|
return result;
|
|
}
|
|
|
|
public TargetBodyPart? GetTargetBodyPart(Entity<BodyPartComponent> part) => GetTargetBodyPart(part.Comp.PartType, part.Comp.Symmetry);
|
|
public TargetBodyPart? GetTargetBodyPart(BodyPartComponent part) => GetTargetBodyPart(part.PartType, part.Symmetry);
|
|
|
|
/// <summary>
|
|
/// Converts Enums from BodyPartType to their Targeting system equivalent.
|
|
/// </summary>
|
|
public TargetBodyPart? GetTargetBodyPart(BodyPartType type, BodyPartSymmetry symmetry)
|
|
{
|
|
return (type, symmetry) switch
|
|
{
|
|
(BodyPartType.Head, _) => TargetBodyPart.Head,
|
|
(BodyPartType.Torso, _) => TargetBodyPart.Torso,
|
|
(BodyPartType.Arm, BodyPartSymmetry.Left) => TargetBodyPart.LeftArm,
|
|
(BodyPartType.Arm, BodyPartSymmetry.Right) => TargetBodyPart.RightArm,
|
|
(BodyPartType.Hand, BodyPartSymmetry.Left) => TargetBodyPart.LeftHand,
|
|
(BodyPartType.Hand, BodyPartSymmetry.Right) => TargetBodyPart.RightHand,
|
|
(BodyPartType.Leg, BodyPartSymmetry.Left) => TargetBodyPart.LeftLeg,
|
|
(BodyPartType.Leg, BodyPartSymmetry.Right) => TargetBodyPart.RightLeg,
|
|
(BodyPartType.Foot, BodyPartSymmetry.Left) => TargetBodyPart.LeftFoot,
|
|
(BodyPartType.Foot, BodyPartSymmetry.Right) => TargetBodyPart.RightFoot,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts Enums from Targeting system to their BodyPartType equivalent.
|
|
/// </summary>
|
|
public (BodyPartType Type, BodyPartSymmetry Symmetry) ConvertTargetBodyPart(TargetBodyPart targetPart)
|
|
{
|
|
return targetPart switch
|
|
{
|
|
TargetBodyPart.Head => (BodyPartType.Head, BodyPartSymmetry.None),
|
|
TargetBodyPart.Torso => (BodyPartType.Torso, BodyPartSymmetry.None),
|
|
TargetBodyPart.Groin => (BodyPartType.Torso, BodyPartSymmetry.None), // TODO: Groin is not a part type yet
|
|
TargetBodyPart.LeftArm => (BodyPartType.Arm, BodyPartSymmetry.Left),
|
|
TargetBodyPart.LeftHand => (BodyPartType.Hand, BodyPartSymmetry.Left),
|
|
TargetBodyPart.RightArm => (BodyPartType.Arm, BodyPartSymmetry.Right),
|
|
TargetBodyPart.RightHand => (BodyPartType.Hand, BodyPartSymmetry.Right),
|
|
TargetBodyPart.LeftLeg => (BodyPartType.Leg, BodyPartSymmetry.Left),
|
|
TargetBodyPart.LeftFoot => (BodyPartType.Foot, BodyPartSymmetry.Left),
|
|
TargetBodyPart.RightLeg => (BodyPartType.Leg, BodyPartSymmetry.Right),
|
|
TargetBodyPart.RightFoot => (BodyPartType.Foot, BodyPartSymmetry.Right),
|
|
_ => (BodyPartType.Torso, BodyPartSymmetry.None)
|
|
};
|
|
|
|
}
|
|
|
|
public DamageSpecifier GetHealingSpecifier(BodyPartComponent part)
|
|
{
|
|
var damage = new DamageSpecifier()
|
|
{
|
|
DamageDict = new Dictionary<string, FixedPoint2>()
|
|
{
|
|
{ "Blunt", -part.SelfHealingAmount },
|
|
{ "Slash", -part.SelfHealingAmount },
|
|
{ "Piercing", -part.SelfHealingAmount },
|
|
{ "Heat", -part.SelfHealingAmount },
|
|
{ "Cold", -part.SelfHealingAmount },
|
|
{ "Shock", -part.SelfHealingAmount },
|
|
{ "Caustic", -part.SelfHealingAmount * 0.1}, // not much caustic healing
|
|
}
|
|
};
|
|
|
|
return damage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches the damage multiplier for part integrity based on part types.
|
|
/// </summary>
|
|
/// TODO: Serialize this per body part.
|
|
public static float GetPartDamageModifier(BodyPartType partType)
|
|
{
|
|
return partType switch
|
|
{
|
|
BodyPartType.Head => 0.5f, // 50% damage, necks are hard to cut
|
|
BodyPartType.Torso => 1.0f, // 100% damage
|
|
BodyPartType.Arm => 0.7f, // 70% damage
|
|
BodyPartType.Hand => 0.7f, // 70% damage
|
|
BodyPartType.Leg => 0.7f, // 70% damage
|
|
BodyPartType.Foot => 0.7f, // 70% damage
|
|
_ => 0.5f
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches the TargetIntegrity equivalent of the current integrity value for the body part.
|
|
/// </summary>
|
|
public static TargetIntegrity GetIntegrityThreshold(BodyPartComponent component, float integrity, bool severed)
|
|
{
|
|
if (severed)
|
|
return TargetIntegrity.Severed;
|
|
else if (!component.Enabled)
|
|
return TargetIntegrity.Disabled;
|
|
|
|
var targetIntegrity = TargetIntegrity.Healthy;
|
|
foreach (var threshold in component.IntegrityThresholds)
|
|
{
|
|
if (integrity <= threshold.Value)
|
|
targetIntegrity = threshold.Key;
|
|
}
|
|
|
|
return targetIntegrity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches the chance to evade integrity damage for a body part.
|
|
/// Used when the entity is not dead, laying down, or incapacitated.
|
|
/// </summary>
|
|
public static float GetEvadeChance(BodyPartType partType)
|
|
{
|
|
return partType switch
|
|
{
|
|
BodyPartType.Head => 0.70f, // 70% chance to evade
|
|
BodyPartType.Arm => 0f, // 0% chance to evade
|
|
BodyPartType.Hand => 0f, // 0% chance to evade
|
|
BodyPartType.Leg => 0f, // 0% chance to evade
|
|
BodyPartType.Foot => 0f, // 0% chance to evade
|
|
BodyPartType.Torso => 0f, // 0% chance to evade
|
|
_ => 0f
|
|
};
|
|
}
|
|
|
|
public bool CanEvadeDamage(EntityUid uid)
|
|
{
|
|
return !_mobState.IsIncapacitated(uid) && !_standing.IsDown(uid);
|
|
}
|
|
|
|
public bool TryEvadeDamage(EntityUid uid, float evadeChance)
|
|
{
|
|
if (!CanEvadeDamage(uid))
|
|
return false;
|
|
|
|
if (evadeChance == 0f)
|
|
return false;
|
|
|
|
var rand = new System.Random((int) _timing.CurTick.Value);
|
|
|
|
return rand.Prob(evadeChance);
|
|
}
|
|
|
|
}
|