Merge branch 'master' into mathmerge

This commit is contained in:
Pieter-Jan Briers
2020-08-20 20:33:43 +02:00
808 changed files with 18173 additions and 5666 deletions

View File

@@ -112,19 +112,14 @@ namespace Content.Server.AI.Utility.Actions
UpdateBlackboard(context);
var considerations = GetConsiderations(context);
DebugTools.Assert(considerations.Count > 0);
// I used the IAUS video although I did have some confusion on how to structure it overall
// as some of the slides seemed contradictory
// Ideally we should early-out each action as cheaply as possible if it's not valid
// We also need some way to tell if the action isn't going to
// have a better score than the current action (if applicable) and early-out that way as well.
// 23:00 Building a better centaur
// Overall structure is based on Building a better centaur
// Ideally we should early-out each action as cheaply as possible if it's not valid, thus
// the finalScore can only go down over time.
var finalScore = 1.0f;
var minThreshold = min / Bonus;
context.GetState<ConsiderationState>().SetValue(considerations.Count);
// See 10:09 for this and the adjustments
foreach (var consideration in considerations)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using Content.Server.AI.Operators;
@@ -6,11 +6,13 @@ using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.BehaviorSets;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.AI;
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -63,6 +65,13 @@ namespace Content.Server.AI.Utility.AiLogic
{
SortActions();
}
if (BehaviorSets.Count == 1 && !EntitySystem.Get<AiSystem>().IsAwake(this))
{
IoCManager.Resolve<IEntityManager>()
.EventBus
.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
}
}
public void RemoveBehaviorSet(Type behaviorSet)
@@ -74,6 +83,13 @@ namespace Content.Server.AI.Utility.AiLogic
BehaviorSets.Remove(behaviorSet);
SortActions();
}
if (BehaviorSets.Count == 0)
{
IoCManager.Resolve<IEntityManager>()
.EventBus
.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
}
}
/// <summary>
@@ -114,38 +130,42 @@ namespace Content.Server.AI.Utility.AiLogic
_planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(SelfEntity);
_planner = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiActionSystem>();
if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.DamageThresholdPassed += DamageThresholdHandle;
damageableComponent.HealthChangedEvent += DeathHandle;
}
}
public override void Shutdown()
{
// TODO: If DamageableComponent removed still need to unsubscribe?
if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
damageableComponent.DamageThresholdPassed -= DamageThresholdHandle;
damageableComponent.HealthChangedEvent -= DeathHandle;
}
var currentOp = CurrentAction?.ActionOperators.Peek();
currentOp?.Shutdown(Outcome.Failed);
}
private void DamageThresholdHandle(object sender, DamageThresholdPassedEventArgs eventArgs)
private void DeathHandle(HealthChangedEventArgs eventArgs)
{
if (!SelfEntity.TryGetComponent(out SpeciesComponent speciesComponent))
{
return;
}
var oldDeadState = _isDead;
_isDead = eventArgs.Damageable.CurrentDamageState == DamageState.Dead || eventArgs.Damageable.CurrentDamageState == DamageState.Critical;
if (speciesComponent.CurrentDamageState is DeadState)
if (oldDeadState != _isDead)
{
_isDead = true;
}
else
{
_isDead = false;
var entityManager = IoCManager.Resolve<IEntityManager>();
switch (_isDead)
{
case true:
entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
break;
case false:
entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
break;
}
}
}
@@ -180,16 +200,6 @@ namespace Content.Server.AI.Utility.AiLogic
public override void Update(float frameTime)
{
// If we can't do anything then there's no point thinking
if (_isDead || BehaviorSets.Count == 0)
{
_actionCancellation?.Cancel();
_blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
CurrentAction?.Shutdown();
CurrentAction = null;
return;
}
// If we asked for a new action we don't want to dump the existing one.
if (_actionRequest != null)
{

View File

@@ -1,4 +1,4 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Damage;
@@ -11,13 +11,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out DamageableComponent damageableComponent))
if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
// Just went with max health
return damageableComponent.CurrentDamage[DamageType.Total] / 300.0f;
return damageableComponent.TotalDamage / 300.0f;
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +10,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
if (speciesComponent.CurrentDamageState is CriticalState)
if (damageableComponent.CurrentDamageState == DamageState.Critical)
{
return 1.0f;
}

View File

@@ -1,6 +1,6 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +10,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
if (speciesComponent.CurrentDamageState is DeadState)
if (damageableComponent.CurrentDamageState == DamageState.Dead)
{
return 1.0f;
}

View File

@@ -13,18 +13,27 @@ namespace Content.Server.AI.Utility.Considerations
private float GetAdjustedScore(Blackboard context)
{
var score = GetScore(context);
/*
* Now using the geometric mean
* for n scores you take the n-th root of the scores multiplied
* e.g. a, b, c scores you take Math.Pow(a * b * c, 1/3)
* To get the ACTUAL geometric mean at any one stage you'd need to divide by the running consideration count
* however, the downside to this is it will fluctuate up and down over time.
* For our purposes if we go below the minimum threshold we want to cut it off, thus we take a
* "running geometric mean" which can only ever go down (and by the final value will equal the actual geometric mean).
*/
// Previously we used a makeupvalue method although the geometric mean is less punishing for more considerations
var considerationsCount = context.GetState<ConsiderationState>().GetValue();
var modificationFactor = 1.0f - 1.0f / considerationsCount;
var makeUpValue = (1.0f - score) * modificationFactor;
var adjustedScore = score + makeUpValue * score;
return MathHelper.Clamp(adjustedScore, 0.0f, 1.0f);
var adjustedScore = MathF.Pow(score, 1 / (float) considerationsCount);
return FloatMath.Clamp(adjustedScore, 0.0f, 1.0f);
}
[Pure]
private static float BoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
return x == 1.0f ? 1.0f : 0.0f;
return x > 0.0f ? 1.0f : 0.0f;
}
public Func<float> BoolCurve(Blackboard context)
@@ -42,7 +51,7 @@ namespace Content.Server.AI.Utility.Considerations
private static float InverseBoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
return x == 1.0f ? 0.0f : 1.0f;
return x == 0.0f ? 1.0f : 0.0f;
}
public Func<float> InverseBoolCurve(Blackboard context)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Combat.Melee;
@@ -7,8 +7,8 @@ using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utils;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects;
using Robust.Shared.IoC;
@@ -37,7 +37,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException();
}
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent),
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IDamageableComponent),
controller.VisionRadius))
{
if (entity.HasComponent<BasicActorComponent>() && entity != owner)

View File

@@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
{
var owner = context.GetState<SelfState>().GetValue();
foreach (var entity in context.GetState<NearbySpeciesState>().GetValue())
foreach (var entity in context.GetState<NearbyBodiesState>().GetValue())
{
yield return new MeleeWeaponAttackEntity(owner, entity, Bonus);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Combat.Melee;
@@ -7,8 +7,8 @@ using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utils;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Body;
using Robust.Server.GameObjects;
using Robust.Shared.IoC;
@@ -37,7 +37,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException();
}
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent),
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IBodyManagerComponent),
controller.VisionRadius))
{
if (entity.HasComponent<BasicActorComponent>() && entity != owner)

View File

@@ -16,6 +16,9 @@ namespace Content.Server.AI.WorldState.States.Inventory
{
foreach (var item in handsComponent.GetAllHeldItems())
{
if (item.Owner.Deleted)
continue;
yield return item.Owner;
}
}

View File

@@ -1,16 +1,16 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.AI.Utils;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Body;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.AI.WorldState.States.Mobs
{
[UsedImplicitly]
public sealed class NearbySpeciesState : CachedStateData<List<IEntity>>
public sealed class NearbyBodiesState : CachedStateData<List<IEntity>>
{
public override string Name => "NearbySpecies";
public override string Name => "NearbyBodies";
protected override List<IEntity> GetTrueValue()
{
@@ -21,7 +21,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
return result;
}
foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.GridPosition, typeof(SpeciesComponent), controller.VisionRadius))
foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.GridPosition, typeof(IBodyManagerComponent), controller.VisionRadius))
{
if (entity == Owner) continue;
result.Add(entity);

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Damage;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
@@ -27,7 +27,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
foreach (var player in nearbyPlayers)
{
if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent<SpeciesComponent>())
if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent<IDamageableComponent>())
{
result.Add(player.AttachedEntity);
}

View File

@@ -0,0 +1,40 @@
#nullable enable
using Content.Server.GameTicking;
using Content.Server.Interfaces.GameTicking;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
namespace Content.Server.Administration
{
public class ReadyAll : IClientCommand
{
public string Command => "readyall";
public string Description => "Readies up all players in the lobby.";
public string Help => $"{Command} | ̣{Command} <ready>";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
var ready = true;
if (args.Length > 0)
{
ready = bool.Parse(args[0]);
}
var gameTicker = IoCManager.Resolve<IGameTicker>();
var playerManager = IoCManager.Resolve<IPlayerManager>();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.SendText(player, "This command can only be ran while in the lobby!");
return;
}
foreach (var p in playerManager.GetAllPlayers())
{
gameTicker.ToggleReady(p, ready);
}
}
}
}

View File

@@ -138,7 +138,7 @@ namespace Content.Server.Atmos
}
}
public class FillGas : IClientCommand
public class FillGas : IClientCommand
{
public string Command => "fillgas";
public string Description => "Adds gas to all tiles in a grid.";

View File

@@ -20,14 +20,16 @@ namespace Content.Server.Atmos
return coordinates.GetTileAtmosphere()?.Air;
}
public static bool TryGetTileAtmosphere(this GridCoordinates coordinates, [NotNullWhen(true)] out TileAtmosphere atmosphere)
public static bool TryGetTileAtmosphere(this GridCoordinates coordinates, [MaybeNullWhen(false)] out TileAtmosphere atmosphere)
{
return (atmosphere = coordinates.GetTileAtmosphere()!) != default;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
return !Equals(atmosphere = coordinates.GetTileAtmosphere()!, default);
}
public static bool TryGetTileAir(this GridCoordinates coordinates, [NotNullWhen(true)] out GasMixture air)
public static bool TryGetTileAir(this GridCoordinates coordinates, [MaybeNullWhen(false)] out GasMixture air)
{
return !(air = coordinates.GetTileAir()!).Equals(default);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
return !Equals(air = coordinates.GetTileAir()!, default);
}
public static TileAtmosphere? GetTileAtmosphere(this MapIndices indices, GridId gridId)
@@ -43,14 +45,16 @@ namespace Content.Server.Atmos
}
public static bool TryGetTileAtmosphere(this MapIndices indices, GridId gridId,
[NotNullWhen(true)] out TileAtmosphere atmosphere)
[MaybeNullWhen(false)] out TileAtmosphere atmosphere)
{
return (atmosphere = indices.GetTileAtmosphere(gridId)!) != default;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
return !Equals(atmosphere = indices.GetTileAtmosphere(gridId)!, default);
}
public static bool TryGetTileAir(this MapIndices indices, GridId gridId, [NotNullWhen(true)] out GasMixture air)
public static bool TryGetTileAir(this MapIndices indices, GridId gridId, [MaybeNullWhen(false)] out GasMixture air)
{
return !(air = indices.GetTileAir(gridId)!).Equals(default);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
return !Equals(air = indices.GetTileAir(gridId)!, default);
}
}
}

View File

@@ -0,0 +1,74 @@
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.Interfaces;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Pointing;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Server.Atmos
{
[RegisterComponent]
public class GasSprayerComponent : Component, IAfterInteract
{
#pragma warning disable 649
[Dependency] private readonly IServerNotifyManager _notifyManager = default!;
[Dependency] private readonly IServerEntityManager _serverEntityManager = default!;
#pragma warning restore 649
//TODO: create a function that can create a gas based on a solution mix
public override string Name => "GasSprayer";
private string _spraySound;
private string _sprayType;
private string _fuelType;
private string _fuelName;
private int _fuelCost;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _spraySound, "spraySound", string.Empty);
serializer.DataField(ref _sprayType, "sprayType", string.Empty);
serializer.DataField(ref _fuelType, "fuelType", string.Empty);
serializer.DataField(ref _fuelName, "fuelName", "fuel");
serializer.DataField(ref _fuelCost, "fuelCost", 50);
}
public void AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!Owner.TryGetComponent(out SolutionComponent tank))
return;
if (tank.Solution.GetReagentQuantity(_fuelType) == 0)
{
_notifyManager.PopupMessage(Owner, eventArgs.User,
Loc.GetString("{0:theName} is out of {1}!", Owner, _fuelName));
}
else
{
tank.TryRemoveReagent(_fuelType, ReagentUnit.New(_fuelCost));
var playerPos = eventArgs.User.Transform.GridPosition;
var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized;
playerPos.Offset(direction/2);
var spray = _serverEntityManager.SpawnEntity(_sprayType, playerPos);
spray.GetComponent<AppearanceComponent>()
.SetData(ExtinguisherVisuals.Rotation, direction.ToAngle().Degrees);
spray.GetComponent<GasVaporComponent>().StartMove(direction, 5);
EntitySystem.Get<AudioSystem>().PlayFromEntity(_spraySound, Owner);
}
}
}
}

View File

@@ -0,0 +1,120 @@
using Content.Shared.Physics;
using Content.Server.Atmos.Reactions;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.Interfaces;
using Content.Shared.Atmos;
namespace Content.Server.Atmos
{
[RegisterComponent]
class GasVaporComponent : Component, ICollideBehavior, IGasMixtureHolder
{
[Dependency] private readonly IMapManager _mapManager = default!;
public override string Name => "GasVapor";
[ViewVariables] public GasMixture Air { get; set; }
private bool _running;
private Vector2 _direction;
private float _velocity;
private float _disspateTimer = 0;
private float _dissipationInterval;
private Gas _gas;
private float _gasVolume;
private float _gasTemperature;
private float _gasAmount;
public override void Initialize()
{
base.Initialize();
Air = new GasMixture(_gasVolume){Temperature = _gasTemperature};
Air.SetMoles(_gas,_gasAmount);
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _dissipationInterval, "dissipationInterval", 1);
serializer.DataField(ref _gas, "gas", Gas.WaterVapor);
serializer.DataField(ref _gasVolume, "gasVolume", 200);
serializer.DataField(ref _gasTemperature, "gasTemperature", Atmospherics.T20C);
serializer.DataField(ref _gasAmount, "gasAmount", 20);
}
public void StartMove(Vector2 dir, float velocity)
{
_running = true;
_direction = dir;
_velocity = velocity;
if (Owner.TryGetComponent(out ICollidableComponent collidable))
{
var controller = collidable.EnsureController<GasVaporController>();
controller.Move(_direction, _velocity);
}
}
public void Update(float frameTime)
{
if (!_running)
return;
if (Owner.TryGetComponent(out ICollidableComponent collidable))
{
var worldBounds = collidable.WorldAABB;
var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID);
var tiles = mapGrid.GetTilesIntersecting(worldBounds);
foreach (var tile in tiles)
{
var pos = tile.GridIndices.ToGridCoordinates(_mapManager, tile.GridIndex);
var atmos = AtmosHelpers.GetTileAtmosphere(pos);
if (atmos.Air == null)
{
return;
}
if (atmos.Air.React(this) != ReactionResult.NoReaction)
{
Owner.Delete();
}
}
}
_disspateTimer += frameTime;
if (_disspateTimer > _dissipationInterval)
{
Air.SetMoles(_gas, Air.TotalMoles/2 );
}
if (Air.TotalMoles < 1)
{
Owner.Delete();
}
}
void ICollideBehavior.CollideWith(IEntity collidedWith)
{
// Check for collision with a impassable object (e.g. wall) and stop
if (collidedWith.TryGetComponent(out ICollidableComponent collidable) &&
(collidable.CollisionLayer & (int) CollisionGroup.Impassable) != 0 &&
collidable.Hard &&
Owner.TryGetComponent(out ICollidableComponent coll))
{
var controller = coll.EnsureController<GasVaporController>();
controller.Stop();
Owner.Delete();
}
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Content.Server.Atmos
/// State for the fire sprite.
/// </summary>
[ViewVariables]
public int State;
public byte State;
public void Start()
{

View File

@@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Server.Interfaces;
using Content.Shared.Atmos;
using Content.Shared.Audio;
@@ -332,32 +333,40 @@ namespace Content.Server.Atmos
var tile = tiles[i];
tile._tileAtmosInfo.FastDone = true;
if (!(tile._tileAtmosInfo.MoleDelta > 0)) continue;
var eligibleDirections = new List<Direction>();
var amtEligibleAdj = 0;
var eligibleDirections = ArrayPool<Direction>.Shared.Rent(4);
var eligibleDirectionCount = 0;
foreach (var direction in Cardinal)
{
if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
// skip anything that isn't part of our current processing block. Original one didn't do this unfortunately, which probably cause some massive lag.
// skip anything that isn't part of our current processing block.
if (tile2._tileAtmosInfo.FastDone || tile2._tileAtmosInfo.LastQueueCycle != queueCycle)
continue;
eligibleDirections.Add(direction);
amtEligibleAdj++;
eligibleDirections[eligibleDirectionCount++] = direction;
}
if (amtEligibleAdj <= 0)
if (eligibleDirectionCount <= 0)
continue; // Oof we've painted ourselves into a corner. Bad luck. Next part will handle this.
var molesToMove = tile._tileAtmosInfo.MoleDelta / amtEligibleAdj;
var molesToMove = tile._tileAtmosInfo.MoleDelta / eligibleDirectionCount;
foreach (var direction in Cardinal)
{
if (eligibleDirections.Contains(direction) ||
!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
var hasDirection = false;
for (var j = 0; j < eligibleDirectionCount; j++)
{
if (eligibleDirections[j] != direction) continue;
hasDirection = true;
break;
}
if (hasDirection || !tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
tile.AdjustEqMovement(direction, molesToMove);
tile._tileAtmosInfo.MoleDelta -= molesToMove;
tile2._tileAtmosInfo.MoleDelta += molesToMove;
}
ArrayPool<Direction>.Shared.Return(eligibleDirections);
}
giverTilesLength = 0;
@@ -446,7 +455,7 @@ namespace Content.Server.Atmos
}
}
ArrayPool<TileAtmosphere>.Shared.Return(queue, true);
ArrayPool<TileAtmosphere>.Shared.Return(queue);
}
else
{
@@ -516,7 +525,7 @@ namespace Content.Server.Atmos
}
}
ArrayPool<TileAtmosphere>.Shared.Return(queue, true);
ArrayPool<TileAtmosphere>.Shared.Return(queue);
}
for (var i = 0; i < tileCount; i++)
@@ -537,9 +546,9 @@ namespace Content.Server.Atmos
}
}
ArrayPool<TileAtmosphere>.Shared.Return(tiles, true);
ArrayPool<TileAtmosphere>.Shared.Return(giverTiles, true);
ArrayPool<TileAtmosphere>.Shared.Return(takerTiles, true);
ArrayPool<TileAtmosphere>.Shared.Return(tiles);
ArrayPool<TileAtmosphere>.Shared.Return(giverTiles);
ArrayPool<TileAtmosphere>.Shared.Return(takerTiles);
}
}
@@ -737,7 +746,7 @@ namespace Content.Server.Atmos
}
else
{
Hotspot.State = Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1;
Hotspot.State = (byte) (Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
}
if (Hotspot.Temperature > MaxFireTemperatureSustained)
@@ -925,16 +934,22 @@ namespace Content.Server.Atmos
public void ExplosivelyDepressurize(int cycleNum)
{
if (Air == null) return;
const int limit = Atmospherics.ZumosTileLimit;
var totalGasesRemoved = 0f;
var queueCycle = ++_gridAtmosphereComponent.EqualizationQueueCycleControl;
var tiles = new List<TileAtmosphere>();
var spaceTiles = new List<TileAtmosphere>();
tiles.Add(this);
var tiles = ArrayPool<TileAtmosphere>.Shared.Rent(limit);
var spaceTiles = ArrayPool<TileAtmosphere>.Shared.Rent(limit);
var tileCount = 0;
var spaceTileCount = 0;
tiles[tileCount++] = this;
ResetTileAtmosInfo();
_tileAtmosInfo.LastQueueCycle = queueCycle;
var tileCount = 1;
for (var i = 0; i < tileCount; i++)
{
var tile = tiles[i];
@@ -942,40 +957,44 @@ namespace Content.Server.Atmos
tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid;
if (tile.Air.Immutable)
{
spaceTiles.Add(tile);
spaceTiles[spaceTileCount++] = tile;
tile.PressureSpecificTarget = tile;
}
else
{
if (i > Atmospherics.ZumosTileLimit) continue;
foreach (var direction in Cardinal)
{
if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
if (tile2?.Air == null) continue;
if (tile2.Air == null) continue;
if (tile2._tileAtmosInfo.LastQueueCycle == queueCycle) continue;
tile.ConsiderFirelocks(tile2);
if (tile._adjacentTiles[direction]?.Air != null)
{
tile2.ResetTileAtmosInfo();
tile2._tileAtmosInfo.LastQueueCycle = queueCycle;
tiles.Add(tile2);
tileCount++;
}
// The firelocks might have closed on us.
if (tile._adjacentTiles[direction]?.Air == null) continue;
tile2.ResetTileAtmosInfo();
tile2._tileAtmosInfo.LastQueueCycle = queueCycle;
tiles[tileCount++] = tile2;
}
}
if (tileCount >= limit || spaceTileCount >= limit)
break;
}
var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl;
var progressionOrder = new List<TileAtmosphere>();
foreach (var tile in spaceTiles)
var progressionOrder = ArrayPool<TileAtmosphere>.Shared.Rent(limit * 2);
var progressionCount = 0;
for (var i = 0; i < spaceTileCount; i++)
{
progressionOrder.Add(tile);
var tile = spaceTiles[i];
progressionOrder[progressionCount++] = tile;
tile._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow;
tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid;
}
var progressionCount = progressionOrder.Count;
for (int i = 0; i < progressionCount; i++)
for (var i = 0; i < progressionCount; i++)
{
var tile = progressionOrder[i];
foreach (var direction in Cardinal)
@@ -988,8 +1007,7 @@ namespace Content.Server.Atmos
tile2._tileAtmosInfo.CurrentTransferAmount = 0;
tile2.PressureSpecificTarget = tile.PressureSpecificTarget;
tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow;
progressionOrder.Add(tile2);
progressionCount++;
progressionOrder[progressionCount++] = tile2;
}
}
@@ -1017,6 +1035,10 @@ namespace Content.Server.Atmos
tile.UpdateVisuals();
tile.HandleDecompressionFloorRip(sum);
}
ArrayPool<TileAtmosphere>.Shared.Return(tiles);
ArrayPool<TileAtmosphere>.Shared.Return(spaceTiles);
ArrayPool<TileAtmosphere>.Shared.Return(progressionOrder);
}
private void HandleDecompressionFloorRip(float sum)
@@ -1029,7 +1051,6 @@ namespace Content.Server.Atmos
private void ConsiderFirelocks(TileAtmosphere other)
{
// TODO ATMOS firelocks!
//throw new NotImplementedException();
}
private void React()

View File

@@ -0,0 +1,147 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Part;
using Content.Shared.GameObjects.Components.Body;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Body
{
class AddHandCommand : IClientCommand
{
public string Command => "addhand";
public string Description => "Adds a hand to your entity.";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.TryIndex("bodyPart.Hand.BasicHuman", out BodyPartPrototype prototype);
var part = new BodyPart(prototype);
var slot = part.GetHashCode().ToString();
body.Template.Slots.Add(slot, BodyPartType.Hand);
body.InstallBodyPart(part, slot);
}
}
class RemoveHandCommand : IClientCommand
{
public string Command => "removehand";
public string Description => "Removes a hand from your entity.";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var hand = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand);
if (hand.Value == null)
{
shell.SendText(player, "You have no hands.");
}
else
{
body.DisconnectBodyPart(hand.Value, true);
}
}
}
class DestroyMechanismCommand : IClientCommand
{
public string Command => "destroymechanism";
public string Description => "Destroys a mechanism from your entity";
public string Help => $"Usage: {Command} <mechanism>";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (args.Length == 0)
{
shell.SendText(player, Help);
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var mechanismName = string.Join(" ", args).ToLowerInvariant();
foreach (var part in body.Parts.Values)
foreach (var mechanism in part.Mechanisms)
{
if (mechanism.Name.ToLowerInvariant() == mechanismName)
{
part.DestroyMechanism(mechanism);
shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed.");
return;
}
}
shell.SendText(player, $"No mechanism was found with name {mechanismName}.");
}
}
}

View File

@@ -0,0 +1,602 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Body.Mechanisms;
using Content.Server.Body.Surgery;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Body.Mechanism;
using Content.Shared.Body.Part;
using Content.Shared.Body.Part.Properties;
using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// Data class representing a singular limb such as an arm or a leg.
/// Typically held within either a <see cref="BodyManagerComponent"/>,
/// which coordinates functions between BodyParts, or a
/// <see cref="DroppedBodyPartComponent"/>.
/// </summary>
public class BodyPart
{
/// <summary>
/// The body that this body part is in, if any.
/// </summary>
private BodyManagerComponent? _body;
/// <summary>
/// Set of all <see cref="Mechanism"/> currently inside this
/// <see cref="BodyPart"/>.
/// To add and remove from this list see <see cref="AddMechanism"/> and
/// <see cref="RemoveMechanism"/>
/// </summary>
private readonly HashSet<Mechanism> _mechanisms = new HashSet<Mechanism>();
public BodyPart(BodyPartPrototype data)
{
SurgeryData = null!;
Properties = new HashSet<IExposeData>();
Name = null!;
Plural = null!;
RSIPath = null!;
RSIState = null!;
RSIMap = null!;
Damage = null!;
Resistances = null!;
LoadFromPrototype(data);
}
/// <summary>
/// The body that this body part is in, if any.
/// </summary>
[ViewVariables]
public BodyManagerComponent? Body
{
get => _body;
set
{
var old = _body;
_body = value;
if (value == null && old != null)
{
foreach (var mechanism in Mechanisms)
{
mechanism.RemovedFromBody(old);
}
}
else
{
foreach (var mechanism in Mechanisms)
{
mechanism.InstalledIntoBody();
}
}
}
}
/// <summary>
/// The <see cref="Surgery.SurgeryData"/> class currently representing this BodyPart's
/// surgery status.
/// </summary>
[ViewVariables] private SurgeryData SurgeryData { get; set; }
/// <summary>
/// How much space is currently taken up by Mechanisms in this BodyPart.
/// </summary>
[ViewVariables] private int SizeUsed { get; set; }
/// <summary>
/// List of <see cref="IExposeData"/> properties, allowing for additional
/// data classes to be attached to a limb, such as a "length" class to an arm.
/// </summary>
[ViewVariables]
private HashSet<IExposeData> Properties { get; }
/// <summary>
/// The name of this <see cref="BodyPart"/>, often displayed to the user.
/// For example, it could be named "advanced robotic arm".
/// </summary>
[ViewVariables]
public string Name { get; private set; }
/// <summary>
/// Plural version of this <see cref="BodyPart"/> name.
/// </summary>
[ViewVariables]
public string Plural { get; private set; }
/// <summary>
/// Path to the RSI that represents this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public string RSIPath { get; private set; }
/// <summary>
/// RSI state that represents this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public string RSIState { get; private set; }
/// <summary>
/// RSI map keys that this body part changes on the sprite.
/// </summary>
[ViewVariables]
public Enum? RSIMap { get; set; }
/// <summary>
/// RSI color of this body part.
/// </summary>
// TODO: SpriteComponent rework
public Color? RSIColor { get; set; }
/// <summary>
/// <see cref="BodyPartType"/> that this <see cref="BodyPart"/> is considered
/// to be.
/// For example, <see cref="BodyPartType.Arm"/>.
/// </summary>
[ViewVariables]
public BodyPartType PartType { get; private set; }
/// <summary>
/// Determines many things: how many mechanisms can be fit inside this
/// <see cref="BodyPart"/>, whether a body can fit through tiny crevices, etc.
/// </summary>
[ViewVariables]
private int Size { get; set; }
/// <summary>
/// Max HP of this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public int MaxDurability { get; private set; }
/// <summary>
/// Current HP of this <see cref="BodyPart"/> based on sum of all damage types.
/// </summary>
[ViewVariables]
public int CurrentDurability => MaxDurability - Damage.TotalDamage;
// TODO: Individual body part damage
/// <summary>
/// Current damage dealt to this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public DamageContainer Damage { get; private set; }
/// <summary>
/// Armor of this <see cref="BodyPart"/> against damages.
/// </summary>
[ViewVariables]
public ResistanceSet Resistances { get; private set; }
/// <summary>
/// At what HP this <see cref="BodyPart"/> destroyed.
/// </summary>
[ViewVariables]
public int DestroyThreshold { get; private set; }
/// <summary>
/// What types of BodyParts this <see cref="BodyPart"/> can easily attach to.
/// For the most part, most limbs aren't universal and require extra work to
/// attach between types.
/// </summary>
[ViewVariables]
public BodyPartCompatibility Compatibility { get; private set; }
/// <summary>
/// Set of all <see cref="Mechanism"/> currently inside this
/// <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public IReadOnlyCollection<Mechanism> Mechanisms => _mechanisms;
/// <summary>
/// This method is called by <see cref="BodyManagerComponent.Update"/>
/// before <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PreMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// This method is called by <see cref="BodyManagerComponent.Update"/>
/// after <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PostMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// Attempts to add the given <see cref="BodyPartProperty"/>.
/// </summary>
/// <returns>
/// True if a <see cref="BodyPartProperty"/> of that type doesn't exist,
/// false otherwise.
/// </returns>
public bool TryAddProperty(BodyPartProperty property)
{
if (HasProperty(property.GetType()))
{
return false;
}
Properties.Add(property);
return true;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <param name="property">The property if found, null otherwise.</param>
/// <typeparam name="T">The type of the property to find.</typeparam>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty<T>(out T property)
{
property = (T) Properties.First(x => x.GetType() == typeof(T));
return property != null;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty(Type propertyType, out BodyPartProperty property)
{
property = (BodyPartProperty) Properties.First(x => x.GetType() == propertyType);
return property != null;
}
/// <summary>
/// Checks if the given type <see cref="T"/> is on this <see cref="BodyPart"/>.
/// </summary>
/// <typeparam name="T">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </typeparam>
/// <returns>
/// True if this <see cref="BodyPart"/> has a property of type
/// <see cref="T"/>, false otherwise.
/// </returns>
public bool HasProperty<T>() where T : BodyPartProperty
{
return Properties.Count(x => x.GetType() == typeof(T)) > 0;
}
/// <summary>
/// Checks if a subtype of <see cref="BodyPartProperty"/> is on this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="propertyType">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </param>
/// <returns>
/// True if this <see cref="BodyPart"/> has a property of type
/// <see cref="propertyType"/>, false otherwise.
/// </returns>
public bool HasProperty(Type propertyType)
{
return Properties.Count(x => x.GetType() == propertyType) > 0;
}
/// <summary>
/// Checks if another <see cref="BodyPart"/> can be connected to this one.
/// </summary>
/// <param name="toBeConnected">The part to connect.</param>
/// <returns>True if it can be connected, false otherwise.</returns>
public bool CanAttachBodyPart(BodyPart toBeConnected)
{
return SurgeryData.CanAttachBodyPart(toBeConnected);
}
/// <summary>
/// Checks if a <see cref="Mechanism"/> can be installed on this
/// <see cref="BodyPart"/>.
/// </summary>
/// <returns>True if it can be installed, false otherwise.</returns>
public bool CanInstallMechanism(Mechanism mechanism)
{
return SizeUsed + mechanism.Size <= Size &&
SurgeryData.CanInstallMechanism(mechanism);
}
/// <summary>
/// Tries to install a mechanism onto this body part.
/// Call <see cref="TryInstallDroppedMechanism"/> instead if you want to
/// easily install an <see cref="IEntity"/> with a
/// <see cref="DroppedMechanismComponent"/>.
/// </summary>
/// <param name="mechanism">The mechanism to try to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="BodyPart"/>).
/// </returns>
private bool TryInstallMechanism(Mechanism mechanism)
{
if (!CanInstallMechanism(mechanism))
{
return false;
}
AddMechanism(mechanism);
return true;
}
/// <summary>
/// Tries to install a <see cref="DroppedMechanismComponent"/> into this
/// <see cref="BodyPart"/>, potentially deleting the dropped
/// <see cref="IEntity"/>.
/// </summary>
/// <param name="droppedMechanism">The mechanism to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="BodyPart"/>).
/// </returns>
public bool TryInstallDroppedMechanism(DroppedMechanismComponent droppedMechanism)
{
if (!TryInstallMechanism(droppedMechanism.ContainedMechanism))
{
return false; //Installing the mechanism failed for some reason.
}
droppedMechanism.Owner.Delete();
return true;
}
/// <summary>
/// Tries to remove the given <see cref="Mechanism"/> reference from
/// this <see cref="BodyPart"/>.
/// </summary>
/// <returns>
/// The newly spawned <see cref="DroppedMechanismComponent"/>, or null
/// if there was an error in spawning the entity or removing the mechanism.
/// </returns>
public bool TryDropMechanism(IEntity dropLocation, Mechanism mechanismTarget,
[NotNullWhen(true)] out DroppedMechanismComponent dropped)
{
dropped = null!;
if (!_mechanisms.Remove(mechanismTarget))
{
return false;
}
SizeUsed -= mechanismTarget.Size;
var entityManager = IoCManager.Resolve<IEntityManager>();
var position = dropLocation.Transform.GridPosition;
var mechanismEntity = entityManager.SpawnEntity("BaseDroppedMechanism", position);
dropped = mechanismEntity.GetComponent<DroppedMechanismComponent>();
dropped.InitializeDroppedMechanism(mechanismTarget);
return true;
}
/// <summary>
/// Tries to destroy the given <see cref="Mechanism"/> in this
/// <see cref="BodyPart"/>. Does NOT spawn a dropped entity.
/// </summary>
/// <summary>
/// Tries to destroy the given <see cref="Mechanism"/> in this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="mechanismTarget">The mechanism to destroy.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool DestroyMechanism(Mechanism mechanismTarget)
{
if (!RemoveMechanism(mechanismTarget))
{
return false;
}
return true;
}
/// <summary>
/// Checks if the given <see cref="SurgeryType"/> can be used on
/// the current state of this <see cref="BodyPart"/>.
/// </summary>
/// <returns>True if it can be used, false otherwise.</returns>
public bool SurgeryCheck(SurgeryType toolType)
{
return SurgeryData.CheckSurgery(toolType);
}
/// <summary>
/// Attempts to perform surgery on this <see cref="BodyPart"/> with the given
/// tool.
/// </summary>
/// <returns>True if successful, false if there was an error.</returns>
public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon, IEntity performer)
{
return SurgeryData.PerformSurgery(toolType, target, surgeon, performer);
}
private void AddMechanism(Mechanism mechanism)
{
DebugTools.AssertNotNull(mechanism);
_mechanisms.Add(mechanism);
SizeUsed += mechanism.Size;
mechanism.Part = this;
mechanism.EnsureInitialize();
if (Body == null)
{
return;
}
if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
{
return;
}
if (!IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(mapString, out var @enum))
{
Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
return;
}
var message = new MechanismSpriteAddedMessage(@enum);
Body.Owner.SendNetworkMessage(Body, message);
}
/// <summary>
/// Tries to remove the given <see cref="mechanism"/> from this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="mechanism">The mechanism to remove.</param>
/// <returns>True if it was removed, false otherwise.</returns>
private bool RemoveMechanism(Mechanism mechanism)
{
DebugTools.AssertNotNull(mechanism);
if (!_mechanisms.Remove(mechanism))
{
return false;
}
SizeUsed -= mechanism.Size;
mechanism.Part = null;
if (Body == null)
{
return true;
}
if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
{
return true;
}
if (!IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(mapString, out var @enum))
{
Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
return true;
}
var message = new MechanismSpriteRemovedMessage(@enum);
Body.Owner.SendNetworkMessage(Body, message);
return true;
}
/// <summary>
/// Loads the given <see cref="BodyPartPrototype"/>.
/// Current data on this <see cref="BodyPart"/> will be overwritten!
/// </summary>
protected virtual void LoadFromPrototype(BodyPartPrototype data)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
Name = data.Name;
Plural = data.Plural;
PartType = data.PartType;
RSIPath = data.RSIPath;
RSIState = data.RSIState;
MaxDurability = data.Durability;
if (!prototypeManager.TryIndex(data.DamageContainerPresetId,
out DamageContainerPrototype damageContainerData))
{
throw new InvalidOperationException(
$"No {nameof(DamageContainerPrototype)} found with id {data.DamageContainerPresetId}");
}
Damage = new DamageContainer(OnHealthChanged, damageContainerData);
if (!prototypeManager.TryIndex(data.ResistanceSetId, out ResistanceSetPrototype resistancesData))
{
throw new InvalidOperationException(
$"No {nameof(ResistanceSetPrototype)} found with id {data.ResistanceSetId}");
}
Resistances = new ResistanceSet(resistancesData);
Size = data.Size;
Compatibility = data.Compatibility;
Properties.Clear();
Properties.UnionWith(data.Properties);
var surgeryDataType = Type.GetType(data.SurgeryDataName);
if (surgeryDataType == null)
{
throw new InvalidOperationException($"No {nameof(Surgery.SurgeryData)} found with name {data.SurgeryDataName}");
}
if (!surgeryDataType.IsSubclassOf(typeof(SurgeryData)))
{
throw new InvalidOperationException(
$"Class {data.SurgeryDataName} is not a subtype of {nameof(Surgery.SurgeryData)} with id {data.ID}");
}
SurgeryData = IoCManager.Resolve<IDynamicTypeFactory>().CreateInstance<SurgeryData>(surgeryDataType, new object[] {this});
foreach (var id in data.Mechanisms)
{
if (!prototypeManager.TryIndex(id, out MechanismPrototype mechanismData))
{
throw new InvalidOperationException($"No {nameof(MechanismPrototype)} found with id {id}");
}
var mechanism = new Mechanism(mechanismData);
AddMechanism(mechanism);
}
}
private void OnHealthChanged(List<HealthChangeData> changes)
{
// TODO
}
public bool SpawnDropped([NotNullWhen(true)] out IEntity dropped)
{
dropped = default!;
if (Body == null)
{
return false;
}
dropped = IoCManager.Resolve<IEntityManager>().SpawnEntity("BaseDroppedBodyPart", Body.Owner.Transform.GridPosition);
dropped.GetComponent<DroppedBodyPartComponent>().TransferBodyPartData(this);
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Content.Shared.Body.Part;
using Content.Shared.Body.Preset;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// Stores data on what <see cref="BodyPartPrototype"></see> should
/// fill a BodyTemplate.
/// Used for loading complete body presets, like a "basic human" with all
/// human limbs.
/// </summary>
public class BodyPreset
{
public BodyPreset(BodyPresetPrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public string Name { get; private set; }
/// <summary>
/// Maps a template slot to the ID of the <see cref="BodyPart"/> that should
/// fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
/// </summary>
[ViewVariables]
public Dictionary<string, string> PartIDs { get; private set; }
protected virtual void LoadFromPrototype(BodyPresetPrototype data)
{
Name = data.Name;
PartIDs = data.PartIDs;
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Template;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// This class is a data capsule representing the standard format of a
/// <see cref="BodyManagerComponent"/>.
/// For instance, the "humanoid" BodyTemplate defines two arms, each connected to
/// a torso and so on.
/// Capable of loading data from a <see cref="BodyTemplatePrototype"/>.
/// </summary>
public class BodyTemplate
{
public BodyTemplate()
{
Name = "empty";
CenterSlot = "";
Slots = new Dictionary<string, BodyPartType>();
Connections = new Dictionary<string, List<string>>();
Layers = new Dictionary<string, string>();
MechanismLayers = new Dictionary<string, string>();
}
public BodyTemplate(BodyTemplatePrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public string Name { get; private set; }
/// <summary>
/// The name of the center BodyPart. For humans, this is set to "torso".
/// Used in many calculations.
/// </summary>
[ViewVariables]
public string CenterSlot { get; set; }
/// <summary>
/// Maps all parts on this template to its BodyPartType.
/// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid
/// template.
/// </summary>
[ViewVariables]
public Dictionary<string, BodyPartType> Slots { get; private set; }
/// <summary>
/// Maps limb name to the list of their connections to other limbs.
/// For instance, on the humanoid template "torso" is mapped to a list
/// containing "right arm", "left arm", "left leg", and "right leg".
/// This is mapped both ways during runtime, but in the prototype only one
/// way has to be defined, i.e., "torso" to "left arm" will automatically
/// map "left arm" to "torso".
/// </summary>
[ViewVariables]
public Dictionary<string, List<string>> Connections { get; private set; }
[ViewVariables]
public Dictionary<string, string> Layers { get; private set; }
[ViewVariables]
public Dictionary<string, string> MechanismLayers { get; private set; }
public bool Equals(BodyTemplate other)
{
return GetHashCode() == other.GetHashCode();
}
/// <summary>
/// Checks if the given slot exists in this <see cref="BodyTemplate"/>.
/// </summary>
/// <returns>True if it does, false otherwise.</returns>
public bool SlotExists(string slotName)
{
return Slots.Keys.Any(slot => slot == slotName);
}
/// <summary>
/// Calculates the hash code for this instance of <see cref="BodyTemplate"/>.
/// It does not matter in which order the Connections or Slots are defined.
/// </summary>
/// <returns>
/// An integer unique to this <see cref="BodyTemplate"/>'s layout.
/// </returns>
public override int GetHashCode()
{
var slotsHash = 0;
var connectionsHash = 0;
foreach (var (key, value) in Slots)
{
var slot = key.GetHashCode();
slot = HashCode.Combine(slot, value.GetHashCode());
slotsHash ^= slot;
}
var connections = new List<int>();
foreach (var (key, value) in Connections)
{
foreach (var targetBodyPart in value)
{
var connection = key.GetHashCode() ^ targetBodyPart.GetHashCode();
if (!connections.Contains(connection))
{
connections.Add(connection);
}
}
}
foreach (var connection in connections)
{
connectionsHash ^= connection;
}
// One of the unit tests considers 0 to be an error, but it will be 0 if
// the BodyTemplate is empty, so let's shift that up to 1.
var hash = HashCode.Combine(
CenterSlot.GetHashCode(),
slotsHash,
connectionsHash);
if (hash == 0)
{
hash++;
}
return hash;
}
protected virtual void LoadFromPrototype(BodyTemplatePrototype data)
{
Name = data.Name;
CenterSlot = data.CenterSlot;
Slots = data.Slots;
Connections = data.Connections;
Layers = data.Layers;
MechanismLayers = data.MechanismLayers;
}
}
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Body.Surgery;
using Content.Server.GameObjects.Components.Body;
namespace Content.Server.Body
{
/// <summary>
/// Making a class inherit from this interface allows you to do many things with
/// it in the <see cref="SurgeryData"/> class.
/// This includes passing it as an argument to a
/// <see cref="SurgeryData.SurgeryAction"/> delegate, as to later typecast it back
/// to the original class type.
/// Every BodyPart also needs an <see cref="IBodyPartContainer"/> to be its parent
/// (i.e. the <see cref="BodyManagerComponent"/> holds many <see cref="BodyPart"/>,
/// each of which have an upward reference to it).
/// </summary>
public interface IBodyPartContainer
{
}
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Body.Mechanisms.Behaviors
{
/// <summary>
/// The behaviors of a brain, inhabitable by a player.
/// </summary>
public class BrainBehavior : MechanismBehavior
{
}
}

View File

@@ -0,0 +1,38 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Circulatory;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class HeartBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
protected override Type? Network => typeof(CirculatoryNetwork);
public override void PreMetabolism(float frameTime)
{
// TODO do between pre and metabolism
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
// Update at most once per second
_accumulatedFrameTime += frameTime;
// TODO: Move/accept/process bloodstream reagents only when the heart is pumping
if (_accumulatedFrameTime >= 1)
{
// bloodstream.Update(_accumulatedFrameTime);
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Respiratory;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class LungBehavior : MechanismBehavior
{
protected override Type? Network => typeof(RespiratoryNetwork);
public override void PreMetabolism(float frameTime)
{
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out LungComponent? lung))
{
return;
}
lung.Update(frameTime);
}
}
}

View File

@@ -0,0 +1,185 @@
#nullable enable
using System;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
namespace Content.Server.Body.Mechanisms.Behaviors
{
/// <summary>
/// The behaviors a mechanism performs.
/// </summary>
public abstract class MechanismBehavior
{
private bool Initialized { get; set; }
private bool Removed { get; set; }
/// <summary>
/// The network, if any, that this behavior forms when its mechanism is
/// added and destroys when its mechanism is removed.
/// </summary>
protected virtual Type? Network { get; } = null;
/// <summary>
/// Upward reference to the parent <see cref="Mechanisms.Mechanism"/> that this
/// behavior is attached to.
/// </summary>
protected Mechanism Mechanism { get; private set; } = null!;
/// <summary>
/// Called by a <see cref="Mechanism"/> to initialize this behavior.
/// </summary>
/// <param name="mechanism">The mechanism that owns this behavior.</param>
/// <exception cref="InvalidOperationException">
/// If the mechanism has already been initialized.
/// </exception>
public void Initialize(Mechanism mechanism)
{
if (Initialized)
{
throw new InvalidOperationException("This mechanism has already been initialized.");
}
Mechanism = mechanism;
Initialize();
if (Mechanism.Body != null)
{
OnInstalledIntoBody();
}
if (Mechanism.Part != null)
{
OnInstalledIntoPart();
}
Initialized = true;
}
/// <summary>
/// Called when a behavior is removed from a <see cref="Mechanism"/>.
/// </summary>
public void Remove()
{
OnRemove();
TryRemoveNetwork(Mechanism.Body);
Mechanism = null!;
Removed = true;
}
/// <summary>
/// Called when the containing <see cref="BodyPart"/> is attached to a
/// <see cref="BodyManagerComponent"/>.
/// For instance, attaching a head to a body will call this on the brain inside.
/// </summary>
public void InstalledIntoBody()
{
TryAddNetwork();
OnInstalledIntoBody();
}
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// installed into a <see cref="BodyPart"/>.
/// For instance, putting a brain into an empty head.
/// </summary>
public void InstalledIntoPart()
{
TryAddNetwork();
OnInstalledIntoPart();
}
/// <summary>
/// Called when the containing <see cref="BodyPart"/> is removed from a
/// <see cref="BodyManagerComponent"/>.
/// For instance, cutting off ones head will call this on the brain inside.
/// </summary>
public void RemovedFromBody(BodyManagerComponent old)
{
OnRemovedFromBody(old);
TryRemoveNetwork(old);
}
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is removed from a
/// <see cref="BodyPart"/>.
/// For instance, taking a brain out of ones head.
/// </summary>
public void RemovedFromPart(BodyPart old)
{
OnRemovedFromPart(old);
TryRemoveNetwork(old.Body);
}
private void TryAddNetwork()
{
if (Network != null)
{
Mechanism.Body?.EnsureNetwork(Network);
}
}
private void TryRemoveNetwork(BodyManagerComponent? body)
{
if (Network != null)
{
body?.RemoveNetwork(Network);
}
}
/// <summary>
/// Called by <see cref="Initialize"/> when this behavior is first initialized.
/// </summary>
protected virtual void Initialize() { }
protected virtual void OnRemove() { }
/// <summary>
/// Called when the containing <see cref="BodyPart"/> is attached to a
/// <see cref="BodyManagerComponent"/>.
/// For instance, attaching a head to a body will call this on the brain inside.
/// </summary>
protected virtual void OnInstalledIntoBody() { }
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// installed into a <see cref="BodyPart"/>.
/// For instance, putting a brain into an empty head.
/// </summary>
protected virtual void OnInstalledIntoPart() { }
/// <summary>
/// Called when the containing <see cref="BodyPart"/> is removed from a
/// <see cref="BodyManagerComponent"/>.
/// For instance, cutting off ones head will call this on the brain inside.
/// </summary>
protected virtual void OnRemovedFromBody(BodyManagerComponent old) { }
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is removed from a
/// <see cref="BodyPart"/>.
/// For instance, taking a brain out of ones head.
/// </summary>
protected virtual void OnRemovedFromPart(BodyPart old) { }
/// <summary>
/// Called every update when this behavior is connected to a
/// <see cref="BodyManagerComponent"/>, but not while in a
/// <see cref="DroppedMechanismComponent"/> or
/// <see cref="DroppedBodyPartComponent"/>,
/// before <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public virtual void PreMetabolism(float frameTime) { }
/// <summary>
/// Called every update when this behavior is connected to a
/// <see cref="BodyManagerComponent"/>, but not while in a
/// <see cref="DroppedMechanismComponent"/> or
/// <see cref="DroppedBodyPartComponent"/>,
/// after <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public virtual void PostMetabolism(float frameTime) { }
}
}

View File

@@ -0,0 +1,36 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Digestive;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class StomachBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
protected override Type? Network => typeof(DigestiveNetwork);
public override void PreMetabolism(float frameTime)
{
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out StomachComponent? stomach))
{
return;
}
// Update at most once per second
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime >= 1)
{
stomach.Update(_accumulatedFrameTime);
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -0,0 +1,249 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.Body.Mechanisms.Behaviors;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body.Mechanisms
{
/// <summary>
/// Data class representing a persistent item inside a <see cref="BodyPart"/>.
/// This includes livers, eyes, cameras, brains, explosive implants,
/// binary communicators, and other things.
/// </summary>
public class Mechanism
{
private BodyPart? _part;
public Mechanism(MechanismPrototype data)
{
Data = data;
Id = null!;
Name = null!;
Description = null!;
ExamineMessage = null!;
RSIPath = null!;
RSIState = null!;
Behaviors = new List<MechanismBehavior>();
}
[ViewVariables] private bool Initialized { get; set; }
[ViewVariables] private MechanismPrototype Data { get; set; }
[ViewVariables] public string Id { get; private set; }
[ViewVariables] public string Name { get; set; }
/// <summary>
/// Professional description of the <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string Description { get; set; }
/// <summary>
/// The message to display upon examining a mob with this Mechanism installed.
/// If the string is empty (""), no message will be displayed.
/// </summary>
[ViewVariables]
public string ExamineMessage { get; set; }
/// <summary>
/// Path to the RSI that represents this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string RSIPath { get; set; }
/// <summary>
/// RSI state that represents this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string RSIState { get; set; }
/// <summary>
/// Max HP of this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public int MaxDurability { get; set; }
/// <summary>
/// Current HP of this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public int CurrentDurability { get; set; }
/// <summary>
/// At what HP this <see cref="Mechanism"/> is completely destroyed.
/// </summary>
[ViewVariables]
public int DestroyThreshold { get; set; }
/// <summary>
/// Armor of this <see cref="Mechanism"/> against attacks.
/// </summary>
[ViewVariables]
public int Resistance { get; set; }
/// <summary>
/// Determines a handful of things - mostly whether this
/// <see cref="Mechanism"/> can fit into a <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public int Size { get; set; }
/// <summary>
/// What kind of <see cref="BodyPart"/> this <see cref="Mechanism"/> can be
/// easily installed into.
/// </summary>
[ViewVariables]
public BodyPartCompatibility Compatibility { get; set; }
/// <summary>
/// The behaviors that this <see cref="Mechanism"/> performs.
/// </summary>
[ViewVariables]
private List<MechanismBehavior> Behaviors { get; }
public BodyManagerComponent? Body => Part?.Body;
public BodyPart? Part
{
get => _part;
set
{
var old = _part;
_part = value;
if (value == null && old != null)
{
foreach (var behavior in Behaviors)
{
behavior.RemovedFromPart(old);
}
}
else
{
foreach (var behavior in Behaviors)
{
behavior.InstalledIntoPart();
}
}
}
}
public void EnsureInitialize()
{
if (Initialized)
{
return;
}
LoadFromPrototype(Data);
Initialized = true;
}
/// <summary>
/// Loads the given <see cref="MechanismPrototype"/>.
/// Current data on this <see cref="Mechanism"/> will be overwritten!
/// </summary>
private void LoadFromPrototype(MechanismPrototype data)
{
Data = data;
Id = data.ID;
Name = data.Name;
Description = data.Description;
ExamineMessage = data.ExamineMessage;
RSIPath = data.RSIPath;
RSIState = data.RSIState;
MaxDurability = data.Durability;
CurrentDurability = MaxDurability;
DestroyThreshold = data.DestroyThreshold;
Resistance = data.Resistance;
Size = data.Size;
Compatibility = data.Compatibility;
foreach (var behavior in Behaviors.ToArray())
{
RemoveBehavior(behavior);
}
foreach (var mechanismBehaviorName in data.BehaviorClasses)
{
var mechanismBehaviorType = Type.GetType(mechanismBehaviorName);
if (mechanismBehaviorType == null)
{
throw new InvalidOperationException(
$"No {nameof(MechanismBehavior)} found with name {mechanismBehaviorName}");
}
if (!mechanismBehaviorType.IsSubclassOf(typeof(MechanismBehavior)))
{
throw new InvalidOperationException(
$"Class {mechanismBehaviorName} is not a subtype of {nameof(MechanismBehavior)} for mechanism prototype {data.ID}");
}
var newBehavior = IoCManager.Resolve<IDynamicTypeFactory>().CreateInstance<MechanismBehavior>(mechanismBehaviorType);
AddBehavior(newBehavior);
}
}
public void InstalledIntoBody()
{
foreach (var behavior in Behaviors)
{
behavior.InstalledIntoBody();
}
}
public void RemovedFromBody(BodyManagerComponent old)
{
foreach (var behavior in Behaviors)
{
behavior.RemovedFromBody(old);
}
}
/// <summary>
/// This method is called by <see cref="BodyPart.PreMetabolism"/> before
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PreMetabolism(float frameTime)
{
foreach (var behavior in Behaviors)
{
behavior.PreMetabolism(frameTime);
}
}
/// <summary>
/// This method is called by <see cref="BodyPart.PostMetabolism"/> after
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PostMetabolism(float frameTime)
{
foreach (var behavior in Behaviors)
{
behavior.PostMetabolism(frameTime);
}
}
private void AddBehavior(MechanismBehavior behavior)
{
Behaviors.Add(behavior);
behavior.Initialize(this);
}
private bool RemoveBehavior(MechanismBehavior behavior)
{
behavior.Remove();
return Behaviors.Remove(behavior);
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using Content.Server.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body.Network
{
/// <summary>
/// Represents a "network" such as a bloodstream or electrical power that
/// is coordinated throughout an entire <see cref="BodyManagerComponent"/>.
/// </summary>
public abstract class BodyNetwork : IExposeData
{
[ViewVariables]
public abstract string Name { get; }
protected IEntity Owner { get; private set; }
public virtual void ExposeData(ObjectSerializer serializer) { }
public void OnAdd(IEntity entity)
{
Owner = entity;
OnAdd();
}
protected virtual void OnAdd() { }
public virtual void OnRemove() { }
/// <summary>
/// Called every update by <see cref="BodyManagerComponent.Update"/>.
/// </summary>
public virtual void Update(float frameTime) { }
}
public static class BodyNetworkExtensions
{
public static void TryAddNetwork(this IEntity entity, Type type)
{
if (!entity.TryGetComponent(out BodyManagerComponent body))
{
return;
}
body.EnsureNetwork(type);
}
public static void TryAddNetwork<T>(this IEntity entity) where T : BodyNetwork
{
if (!entity.TryGetComponent(out BodyManagerComponent body))
{
return;
}
body.EnsureNetwork<T>();
}
public static bool TryGetBodyNetwork(this IEntity entity, Type type, out BodyNetwork network)
{
network = null;
return entity.TryGetComponent(out BodyManagerComponent body) &&
body.TryGetNetwork(type, out network);
}
public static bool TryGetBodyNetwork<T>(this IEntity entity, out T network) where T : BodyNetwork
{
entity.TryGetBodyNetwork(typeof(T), out var unCastNetwork);
network = (T) unCastNetwork;
return network != null;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
namespace Content.Server.Body.Network
{
public class BodyNetworkFactory : IBodyNetworkFactory
{
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
/// <summary>
/// Mapping of body network names to their types.
/// </summary>
private readonly Dictionary<string, Type> _names = new Dictionary<string, Type>();
private void Register(Type type)
{
if (_names.ContainsValue(type))
{
throw new InvalidOperationException($"Type is already registered: {type}");
}
if (!type.IsSubclassOf(typeof(BodyNetwork)))
{
throw new InvalidOperationException($"{type} is not a subclass of {nameof(BodyNetwork)}");
}
var dummy = _typeFactory.CreateInstance<BodyNetwork>(type);
if (dummy == null)
{
throw new NullReferenceException();
}
var name = dummy.Name;
if (name == null)
{
throw new NullReferenceException($"{type}'s name cannot be null.");
}
if (_names.ContainsKey(name))
{
throw new InvalidOperationException($"{name} is already registered.");
}
_names.Add(name, type);
}
public void DoAutoRegistrations()
{
var bodyNetwork = typeof(BodyNetwork);
foreach (var child in _reflectionManager.GetAllChildren(bodyNetwork))
{
Register(child);
}
}
public BodyNetwork GetNetwork(string name)
{
Type type;
try
{
type = _names[name];
}
catch (KeyNotFoundException)
{
throw new ArgumentException($"No {nameof(BodyNetwork)} exists with name {name}");
}
return _typeFactory.CreateInstance<BodyNetwork>(type);
}
public BodyNetwork GetNetwork(Type type)
{
if (!_names.ContainsValue(type))
{
throw new ArgumentException($"{type} is not registered.");
}
return _typeFactory.CreateInstance<BodyNetwork>(type);
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.GameObjects.Components.Body.Circulatory;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
[UsedImplicitly]
public class CirculatoryNetwork : BodyNetwork
{
public override string Name => "Circulatory";
protected override void OnAdd()
{
Owner.EnsureComponent<BloodstreamComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<BloodstreamComponent>())
{
Owner.RemoveComponent<BloodstreamComponent>();
}
}
}
}

View File

@@ -0,0 +1,28 @@
using Content.Server.GameObjects.Components.Body.Digestive;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
/// <summary>
/// Represents the system that processes food, liquids, and the reagents inside them.
/// </summary>
[UsedImplicitly]
public class DigestiveNetwork : BodyNetwork
{
public override string Name => "Digestive";
protected override void OnAdd()
{
Owner.EnsureComponent<StomachComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<StomachComponent>())
{
Owner.RemoveComponent<StomachComponent>();
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Content.Server.Body.Network
{
public interface IBodyNetworkFactory
{
void DoAutoRegistrations();
BodyNetwork GetNetwork(string name);
BodyNetwork GetNetwork(Type type);
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.GameObjects.Components.Body.Respiratory;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
[UsedImplicitly]
public class RespiratoryNetwork : BodyNetwork
{
public override string Name => "Respiratory";
protected override void OnAdd()
{
Owner.EnsureComponent<LungComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<LungComponent>())
{
Owner.RemoveComponent<LungComponent>();
}
}
}
}

View File

@@ -0,0 +1,250 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Body.Mechanisms;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// Data class representing the surgery state of a biological entity.
/// </summary>
[UsedImplicitly]
public class BiologicalSurgeryData : SurgeryData
{
private readonly List<Mechanism> _disconnectedOrgans = new List<Mechanism>();
private bool _skinOpened;
private bool _skinRetracted;
private bool _vesselsClamped;
public BiologicalSurgeryData(BodyPart parent) : base(parent) { }
protected override SurgeryAction? GetSurgeryStep(SurgeryType toolType)
{
if (toolType == SurgeryType.Amputation)
{
return RemoveBodyPartSurgery;
}
if (!_skinOpened)
{
// Case: skin is normal.
if (toolType == SurgeryType.Incision)
{
return OpenSkinSurgery;
}
}
else if (!_vesselsClamped)
{
// Case: skin is opened, but not clamped.
switch (toolType)
{
case SurgeryType.VesselCompression:
return ClampVesselsSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else if (!_skinRetracted)
{
// Case: skin is opened and clamped, but not retracted.
switch (toolType)
{
case SurgeryType.Retraction:
return RetractSkinSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else
{
// Case: skin is fully open.
if (Parent.Mechanisms.Count > 0 &&
toolType == SurgeryType.VesselCompression)
{
if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
{
return LoosenOrganSurgery;
}
}
if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
{
return RemoveOrganSurgery;
}
if (toolType == SurgeryType.Cauterization)
{
return CauterizeIncisionSurgery;
}
}
return null;
}
public override string GetDescription(IEntity target)
{
var toReturn = "";
if (_skinOpened && !_vesselsClamped)
{
// Case: skin is opened, but not clamped.
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.\n",
target, Parent.Name);
}
else if (_skinOpened && _vesselsClamped && !_skinRetracted)
{
// Case: skin is opened and clamped, but not retracted.
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.\n",
target, Parent.Name);
}
else if (_skinOpened && _vesselsClamped && _skinRetracted)
{
// Case: skin is fully open.
toReturn += Loc.GetString("There is an incision on {0:their} {1}.\n", target, Parent.Name);
foreach (var mechanism in _disconnectedOrgans)
{
toReturn += Loc.GetString("{0:their} {1} is loose.\n", target, mechanism.Name);
}
}
return toReturn;
}
public override bool CanInstallMechanism(Mechanism mechanism)
{
return _skinOpened && _vesselsClamped && _skinRetracted;
}
public override bool CanAttachBodyPart(BodyPart part)
{
return true;
// TODO: if a bodypart is disconnected, you should have to do some surgery to allow another bodypart to be attached.
}
private void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Cut open the skin..."));
// TODO do_after: Delay
_skinOpened = true;
}
private void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Clamp the vessels..."));
// TODO do_after: Delay
_vesselsClamped = true;
}
private void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Retract the skin..."));
// TODO do_after: Delay
_skinRetracted = true;
}
private void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Cauterize the incision..."));
// TODO do_after: Delay
_skinOpened = false;
_vesselsClamped = false;
_skinRetracted = false;
}
private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent.Mechanisms.Count <= 0)
{
return;
}
var toSend = new List<Mechanism>();
foreach (var mechanism in Parent.Mechanisms)
{
if (!_disconnectedOrgans.Contains(mechanism))
{
toSend.Add(mechanism);
}
}
if (toSend.Count > 0)
{
surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
}
}
private void LoosenOrganSurgeryCallback(Mechanism target, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
if (target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(performer, Loc.GetString("Loosen the organ..."));
// TODO do_after: Delay
_disconnectedOrgans.Add(target);
}
private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (_disconnectedOrgans.Count <= 0)
{
return;
}
if (_disconnectedOrgans.Count == 1)
{
RemoveOrganSurgeryCallback(_disconnectedOrgans[0], container, surgeon, performer);
}
else
{
surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
}
}
private void RemoveOrganSurgeryCallback(Mechanism target, IBodyPartContainer container,
ISurgeon surgeon,
IEntity performer)
{
if (target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(performer, Loc.GetString("Remove the organ..."));
// TODO do_after: Delay
Parent.TryDropMechanism(performer, target, out _);
_disconnectedOrgans.Remove(target);
}
private void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
// This surgery requires a DroppedBodyPartComponent.
if (!(container is BodyManagerComponent))
{
return;
}
var bmTarget = (BodyManagerComponent) container;
performer.PopupMessage(performer, Loc.GetString("Saw off the limb!"));
// TODO do_after: Delay
bmTarget.DisconnectBodyPart(Parent, true);
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using Content.Server.Body.Mechanisms;
using Content.Server.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// Interface representing an entity capable of performing surgery (performing operations on an
/// <see cref="SurgeryData"/> class).
/// For an example see <see cref="SurgeryToolComponent"/>, which inherits from this class.
/// </summary>
public interface ISurgeon
{
public delegate void MechanismRequestCallback(
Mechanism target,
IBodyPartContainer container,
ISurgeon surgeon,
IEntity performer);
/// <summary>
/// How long it takes to perform a single surgery step (in seconds).
/// </summary>
public float BaseOperationTime { get; set; }
/// <summary>
/// When performing a surgery, the <see cref="SurgeryData"/> may sometimes require selecting from a set of Mechanisms
/// to operate on.
/// This function is called in that scenario, and it is expected that you call the callback with one mechanism from the
/// provided list.
/// </summary>
public void RequestMechanism(IEnumerable<Mechanism> options, MechanismRequestCallback callback);
}
}

View File

@@ -0,0 +1,91 @@
#nullable enable
using Content.Server.Body.Mechanisms;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// This data class represents the state of a <see cref="BodyPart"/> in regards to everything surgery related -
/// whether there's an incision on it, whether the bone is broken, etc.
/// </summary>
public abstract class SurgeryData
{
protected delegate void SurgeryAction(IBodyPartContainer container, ISurgeon surgeon, IEntity performer);
/// <summary>
/// The <see cref="BodyPart"/> this surgeryData is attached to.
/// The <see cref="SurgeryData"/> class should not exist without a
/// <see cref="BodyPart"/> that it represents, and will throw errors if it
/// is null.
/// </summary>
protected readonly BodyPart Parent;
protected SurgeryData(BodyPart parent)
{
Parent = parent;
}
/// <summary>
/// The <see cref="BodyPartType"/> of the parent <see cref="BodyPart"/>.
/// </summary>
protected BodyPartType ParentType => Parent.PartType;
/// <summary>
/// Returns the description of this current <see cref="BodyPart"/> to be shown
/// upon observing the given entity.
/// </summary>
public abstract string GetDescription(IEntity target);
/// <summary>
/// Returns whether a <see cref="Mechanism"/> can be installed into the
/// <see cref="BodyPart"/> this <see cref="SurgeryData"/> represents.
/// </summary>
public abstract bool CanInstallMechanism(Mechanism mechanism);
/// <summary>
/// Returns whether the given <see cref="BodyPart"/> can be connected to the
/// <see cref="BodyPart"/> this <see cref="SurgeryData"/> represents.
/// </summary>
public abstract bool CanAttachBodyPart(BodyPart part);
/// <summary>
/// Gets the delegate corresponding to the surgery step using the given
/// <see cref="SurgeryType"/>.
/// </summary>
/// <returns>
/// The corresponding surgery action or null if no step can be performed.
/// </returns>
protected abstract SurgeryAction? GetSurgeryStep(SurgeryType toolType);
/// <summary>
/// Returns whether the given <see cref="SurgeryType"/> can be used to perform a surgery on the BodyPart this
/// <see cref="SurgeryData"/> represents.
/// </summary>
public bool CheckSurgery(SurgeryType toolType)
{
return GetSurgeryStep(toolType) != null;
}
/// <summary>
/// Attempts to perform surgery of the given <see cref="SurgeryType"/>. Returns whether the operation was successful.
/// </summary>
/// <param name="surgeryType">The <see cref="SurgeryType"/> used for this surgery.</param>
/// <param name="container">The container where the surgery is being done.</param>
/// <param name="surgeon">The entity being used to perform the surgery.</param>
/// <param name="performer">The entity performing the surgery.</param>
public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
var step = GetSurgeryStep(surgeryType);
if (step == null)
{
return false;
}
step(container, surgeon, performer);
return true;
}
}
}

View File

@@ -1,10 +1,11 @@
using System.Linq;
using Content.Server.GameObjects.Components.Damage;
using System;
using System.Linq;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects;
using Content.Server.Observer;
using Content.Server.Players;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.Interfaces.Console;
@@ -13,6 +14,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Content.Shared.Damage;
namespace Content.Server.Chat
{
@@ -30,9 +32,11 @@ namespace Content.Server.Chat
if (args.Length < 1)
return;
var chat = IoCManager.Resolve<IChatManager>();
var message = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(message))
return;
var message = string.Join(" ", args);
var chat = IoCManager.Resolve<IChatManager>();
if (player.AttachedEntity.HasComponent<GhostComponent>())
chat.SendDeadChat(player, message);
@@ -59,9 +63,11 @@ namespace Content.Server.Chat
if (args.Length < 1)
return;
var chat = IoCManager.Resolve<IChatManager>();
var action = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(action))
return;
var action = string.Join(" ", args);
var chat = IoCManager.Resolve<IChatManager>();
var mindComponent = player.ContentData().Mind;
chat.EntityMe(mindComponent.OwnedEntity, action);
@@ -76,8 +82,15 @@ namespace Content.Server.Chat
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if (args.Length < 1)
return;
var message = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(message))
return;
var chat = IoCManager.Resolve<IChatManager>();
chat.SendOOC(player, string.Join(" ", args));
chat.SendOOC(player, message);
}
}
@@ -89,8 +102,15 @@ namespace Content.Server.Chat
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if (args.Length < 1)
return;
var message = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(message))
return;
var chat = IoCManager.Resolve<IChatManager>();
chat.SendAdminChat(player, string.Join(" ", args));
chat.SendAdminChat(player, message);
}
}
@@ -105,24 +125,24 @@ namespace Content.Server.Chat
"If that fails, it will attempt to use an object in the environment.\n" +
"Finally, if neither of the above worked, you will die by biting your tongue.";
private void DealDamage(ISuicideAct suicide, IChatManager chat, DamageableComponent damageableComponent, IEntity source, IEntity target)
private void DealDamage(ISuicideAct suicide, IChatManager chat, IDamageableComponent damageableComponent, IEntity source, IEntity target)
{
SuicideKind kind = suicide.Suicide(target, chat);
if (kind != SuicideKind.Special)
{
damageableComponent.TakeDamage(kind switch
{
SuicideKind.Brute => DamageType.Brute,
SuicideKind.Heat => DamageType.Heat,
SuicideKind.Cold => DamageType.Cold,
SuicideKind.Acid => DamageType.Acid,
SuicideKind.Toxic => DamageType.Toxic,
SuicideKind.Electric => DamageType.Electric,
_ => DamageType.Brute
},
500, //TODO: needs to be a max damage of some sorts
source,
target);
damageableComponent.ChangeDamage(kind switch
{
SuicideKind.Blunt => DamageType.Blunt,
SuicideKind.Piercing => DamageType.Piercing,
SuicideKind.Heat => DamageType.Heat,
SuicideKind.Disintegration => DamageType.Disintegration,
SuicideKind.Cellular => DamageType.Cellular,
SuicideKind.DNA => DamageType.DNA,
SuicideKind.Asphyxiation => DamageType.Asphyxiation,
_ => DamageType.Blunt
},
500,
true, source);
}
}
@@ -133,7 +153,7 @@ namespace Content.Server.Chat
var chat = IoCManager.Resolve<IChatManager>();
var owner = player.ContentData().Mind.OwnedMob.Owner;
var dmgComponent = owner.GetComponent<DamageableComponent>();
var dmgComponent = owner.GetComponent<IDamageableComponent>();
//TODO: needs to check if the mob is actually alive
//TODO: maybe set a suicided flag to prevent ressurection?
@@ -167,7 +187,11 @@ namespace Content.Server.Chat
}
// Default suicide, bite your tongue
chat.EntityMe(owner, Loc.GetString("is attempting to bite {0:their} own tongue, looks like {0:theyre} trying to commit suicide!", owner)); //TODO: theyre macro
dmgComponent.TakeDamage(DamageType.Brute, 500, owner, owner); //TODO: dmg value needs to be a max damage of some sorts
dmgComponent.ChangeDamage(DamageType.Piercing, 500, true, owner);
// Prevent the player from returning to the body. Yes, this is an ugly hack.
var ghost = new Ghost(){CanReturn = false};
ghost.Execute(shell, player, Array.Empty<string>());
}
}
}

View File

@@ -5,7 +5,9 @@ using Content.Server.Interfaces;
using Content.Server.Interfaces.Chat;
using Content.Shared.Chat;
using Content.Shared.GameObjects.EntitySystems;
using NFluidsynth;
using Robust.Server.Console;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
@@ -19,8 +21,18 @@ namespace Content.Server.Chat
/// </summary>
internal sealed class ChatManager : IChatManager
{
/// <summary>
/// The maximum length a player-sent message can be sent
/// </summary>
public int MaxMessageLength = 1000;
private const int VoiceRange = 7; // how far voice goes in world units
/// <summary>
/// The message displayed to the player when it exceeds the chat character limit
/// </summary>
private const string MaxLengthExceededMessage = "Your message exceeded {0} character limit";
#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
[Dependency] private readonly IServerNetManager _netManager;
@@ -33,6 +45,12 @@ namespace Content.Server.Chat
public void Initialize()
{
_netManager.RegisterNetMessage<MsgChatMessage>(MsgChatMessage.NAME);
_netManager.RegisterNetMessage<ChatMaxMsgLengthMessage>(ChatMaxMsgLengthMessage.NAME, _onMaxLengthRequest);
// Tell all the connected players the chat's character limit
var msg = _netManager.CreateNetMessage<ChatMaxMsgLengthMessage>();
msg.MaxMessageLength = MaxMessageLength;
_netManager.ServerSendToAll(msg);
}
public void DispatchServerAnnouncement(string message)
@@ -69,6 +87,17 @@ namespace Content.Server.Chat
return;
}
// Get entity's PlayerSession
IPlayerSession playerSession = source.GetComponent<IActorComponent>().playerSession;
// Check if message exceeds the character limit if the sender is a player
if (playerSession != null)
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(playerSession, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
return;
}
var pos = source.Transform.GridPosition;
var clients = _playerManager.GetPlayersInRange(pos, VoiceRange).Select(p => p.ConnectedClient);
@@ -90,6 +119,17 @@ namespace Content.Server.Chat
return;
}
// Check if entity is a player
IPlayerSession playerSession = source.GetComponent<IActorComponent>().playerSession;
// Check if message exceeds the character limit
if (playerSession != null)
if (action.Length > MaxMessageLength)
{
DispatchServerMessage(playerSession, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
return;
}
var pos = source.Transform.GridPosition;
var clients = _playerManager.GetPlayersInRange(pos, VoiceRange).Select(p => p.ConnectedClient);
@@ -103,6 +143,13 @@ namespace Content.Server.Chat
public void SendOOC(IPlayerSession player, string message)
{
// Check if message exceeds the character limi
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
return;
}
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
msg.Channel = ChatChannel.OOC;
msg.Message = message;
@@ -114,6 +161,13 @@ namespace Content.Server.Chat
public void SendDeadChat(IPlayerSession player, string message)
{
// Check if message exceeds the character limit
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
return;
}
var clients = _playerManager.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent<GhostComponent>()).Select(p => p.ConnectedClient);;
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
@@ -126,7 +180,14 @@ namespace Content.Server.Chat
public void SendAdminChat(IPlayerSession player, string message)
{
if(!_conGroupController.CanCommand(player, "asay"))
// Check if message exceeds the character limit
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
return;
}
if (!_conGroupController.CanCommand(player, "asay"))
{
SendOOC(player, message);
return;
@@ -149,5 +210,12 @@ namespace Content.Server.Chat
msg.MessageWrap = $"OOC: (D){sender}: {{0}}";
_netManager.ServerSendToAll(msg);
}
private void _onMaxLengthRequest(ChatMaxMsgLengthMessage msg)
{
var response = _netManager.CreateNetMessage<ChatMaxMsgLengthMessage>();
response.MaxMessageLength = MaxMessageLength;
_netManager.ServerSendMessage(response, msg.MsgChannel);
}
}
}

View File

@@ -9,6 +9,7 @@
<OutputPath>..\bin\Content.Server\</OutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<NoWarn>1998</NoWarn>
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>

View File

@@ -1,8 +1,9 @@
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.Interfaces;
using Content.Server.Interfaces.Chat;
using Content.Server.Body.Network;
using Content.Server.Interfaces.GameTicking;
using Content.Server.Interfaces.PDA;
using Content.Server.Sandbox;
@@ -46,6 +47,8 @@ namespace Content.Server
IoCManager.BuildGraph();
IoCManager.Resolve<IBodyNetworkFactory>().DoAutoRegistrations();
_gameTicker = IoCManager.Resolve<IGameTicker>();
IoCManager.Resolve<IServerNotifyManager>().Initialize();

View File

@@ -1,7 +1,7 @@
using System;
using System.Linq;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Maps;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;

View File

@@ -71,16 +71,16 @@ namespace Content.Server.GameObjects.Components.Access
public static ICollection<string> FindAccessTags(IEntity entity)
{
if (entity.TryGetComponent(out IAccess accessComponent))
if (entity.TryGetComponent(out IAccess? accessComponent))
{
return accessComponent.Tags;
}
if (entity.TryGetComponent(out IHandsComponent handsComponent))
if (entity.TryGetComponent(out IHandsComponent? handsComponent))
{
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity != null &&
activeHandEntity.TryGetComponent(out IAccess handAccessComponent))
activeHandEntity.TryGetComponent(out IAccess? handAccessComponent))
{
return handAccessComponent.Tags;
}
@@ -90,11 +90,11 @@ namespace Content.Server.GameObjects.Components.Access
return Array.Empty<string>();
}
if (entity.TryGetComponent(out InventoryComponent inventoryComponent))
if (entity.TryGetComponent(out InventoryComponent? inventoryComponent))
{
if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent item) &&
item.Owner.TryGetComponent(out IAccess idAccessComponent)
item.Owner.TryGetComponent(out IAccess? idAccessComponent)
)
{
return idAccessComponent.Tags;

View File

@@ -1,5 +1,6 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
@@ -14,17 +15,18 @@ namespace Content.Server.GameObjects.Components
{
public override string Name => "Anchorable";
int IInteractUsing.Priority => 1;
/// <summary>
/// Checks if a tool can change the anchored status.
/// </summary>
/// <param name="user">The user doing the action</param>
/// <param name="utilizing">The tool being used, can be null if forcing it</param>
/// <param name="collidable">The physics component of the owning entity</param>
/// <param name="force">Whether or not to check if the tool is valid</param>
/// <returns>true if it is valid, false otherwise</returns>
private bool Valid(IEntity user, IEntity? utilizing, [MaybeNullWhen(false)] out ICollidableComponent collidable, bool force = false)
private async Task<bool> Valid(IEntity user, IEntity? utilizing, [MaybeNullWhen(false)] bool force = false)
{
if (!Owner.TryGetComponent(out collidable))
if (!Owner.HasComponent<ICollidableComponent>())
{
return false;
}
@@ -32,8 +34,8 @@ namespace Content.Server.GameObjects.Components
if (!force)
{
if (utilizing == null ||
!utilizing.TryGetComponent(out ToolComponent tool) ||
!tool.UseTool(user, Owner, ToolQuality.Anchoring))
!utilizing.TryGetComponent(out ToolComponent? tool) ||
!(await tool.UseTool(user, Owner, 0.5f, ToolQuality.Anchoring)))
{
return false;
}
@@ -49,13 +51,14 @@ namespace Content.Server.GameObjects.Components
/// <param name="utilizing">The tool being used, if any</param>
/// <param name="force">Whether or not to ignore valid tool checks</param>
/// <returns>true if anchored, false otherwise</returns>
public bool TryAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
public async Task<bool> TryAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
if (!Valid(user, utilizing, out var physics, force))
if (!(await Valid(user, utilizing, force)))
{
return false;
}
var physics = Owner.GetComponent<ICollidableComponent>();
physics.Anchored = true;
return true;
@@ -68,13 +71,14 @@ namespace Content.Server.GameObjects.Components
/// <param name="utilizing">The tool being used, if any</param>
/// <param name="force">Whether or not to ignore valid tool checks</param>
/// <returns>true if unanchored, false otherwise</returns>
public bool TryUnAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
public async Task<bool> TryUnAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
if (!Valid(user, utilizing, out var physics, force))
if (!(await Valid(user, utilizing, force)))
{
return false;
}
var physics = Owner.GetComponent<ICollidableComponent>();
physics.Anchored = false;
return true;
@@ -87,16 +91,16 @@ namespace Content.Server.GameObjects.Components
/// <param name="utilizing">The tool being used, if any</param>
/// <param name="force">Whether or not to ignore valid tool checks</param>
/// <returns>true if toggled, false otherwise</returns>
private bool TryToggleAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
private async Task<bool> TryToggleAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
if (!Owner.TryGetComponent(out ICollidableComponent collidable))
if (!Owner.TryGetComponent(out ICollidableComponent? collidable))
{
return false;
}
return collidable.Anchored ?
TryUnAnchor(user, utilizing, force) :
TryAnchor(user, utilizing, force);
await TryUnAnchor(user, utilizing, force) :
await TryAnchor(user, utilizing, force);
}
public override void Initialize()
@@ -105,9 +109,9 @@ namespace Content.Server.GameObjects.Components
Owner.EnsureComponent<CollidableComponent>();
}
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryToggleAnchor(eventArgs.User, eventArgs.Using);
return await TryToggleAnchor(eventArgs.User, eventArgs.Using);
}
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
@@ -23,7 +23,7 @@ namespace Content.Server.GameObjects.Components.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(float frameTime)
{
if (!Owner.TryGetComponent(out DamageableComponent damageable)) return;
if (!Owner.TryGetComponent(out IDamageableComponent damageable)) return;
Owner.TryGetComponent(out ServerStatusEffectsComponent status);
var coordinates = Owner.Transform.GridPosition;
@@ -52,7 +52,7 @@ namespace Content.Server.GameObjects.Components.Atmos
if(pressure > Atmospherics.WarningLowPressure)
goto default;
damageable.TakeDamage(DamageType.Brute, Atmospherics.LowPressureDamage, Owner);
damageable.ChangeDamage(DamageType.Blunt, Atmospherics.LowPressureDamage, false, Owner);
if (status == null) break;
@@ -74,7 +74,7 @@ namespace Content.Server.GameObjects.Components.Atmos
var damage = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
damageable.TakeDamage(DamageType.Brute, damage, Owner);
damageable.ChangeDamage(DamageType.Blunt, damage, false, Owner);
if (status == null) break;

View File

@@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Atmos
{
_pressureDanger = GasAnalyzerDanger.Nominal;
}
Dirty();
_timeSinceSync = 0f;
}
@@ -131,11 +131,11 @@ namespace Content.Server.GameObjects.Components.Atmos
if (session.AttachedEntity == null)
return;
if (!session.AttachedEntity.TryGetComponent(out IHandsComponent handsComponent))
if (!session.AttachedEntity.TryGetComponent(out IHandsComponent? handsComponent))
return;
var activeHandEntity = handsComponent?.GetActiveHand?.Owner;
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent gasAnalyzer))
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
return;
}
@@ -147,7 +147,7 @@ namespace Content.Server.GameObjects.Components.Atmos
// Check if position is out of range => don't update
if (!_position.Value.InRange(_mapManager, pos, SharedInteractionSystem.InteractionRange))
return;
pos = _position.Value;
}
@@ -195,7 +195,7 @@ namespace Content.Server.GameObjects.Components.Atmos
return;
}
if (!player.TryGetComponent(out IHandsComponent handsComponent))
if (!player.TryGetComponent(out IHandsComponent? handsComponent))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, player,
Loc.GetString("You have no hands."));
@@ -203,7 +203,7 @@ namespace Content.Server.GameObjects.Components.Atmos
}
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent gasAnalyzer))
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
_notifyManager.PopupMessage(serverMsg.Session.AttachedEntity,
serverMsg.Session.AttachedEntity,
@@ -225,7 +225,7 @@ namespace Content.Server.GameObjects.Components.Atmos
return;
}
if (eventArgs.User.TryGetComponent(out IActorComponent actor))
if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
OpenInterface(actor.playerSession, eventArgs.ClickLocation);
//TODO: show other sprite when ui open?
@@ -236,7 +236,7 @@ namespace Content.Server.GameObjects.Components.Atmos
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out IActorComponent actor))
if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
CloseInterface(actor.playerSession);
//TODO: if other sprite is shown, change again
@@ -245,7 +245,7 @@ namespace Content.Server.GameObjects.Components.Atmos
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out IActorComponent actor))
if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
OpenInterface(actor.playerSession);
//TODO: show other sprite when ui open?

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
@@ -8,7 +9,8 @@ namespace Content.Server.GameObjects.Components.Atmos
public class GasMixtureComponent : Component
{
public override string Name => "GasMixture";
public GasMixture GasMixture { get; set; } = new GasMixture();
[ViewVariables] public GasMixture GasMixture { get; set; } = new GasMixture();
public override void ExposeData(ObjectSerializer serializer)
{

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using Content.Server.Body;
using Content.Shared.Body.Scanner;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Body
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class BodyScannerComponent : Component, IActivate
{
private BoundUserInterface _userInterface;
public sealed override string Name => "BodyScanner";
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent actor) ||
actor.playerSession.AttachedEntity == null)
{
return;
}
if (actor.playerSession.AttachedEntity.TryGetComponent(out BodyManagerComponent attempt))
{
var state = InterfaceState(attempt.Template, attempt.Parts);
_userInterface.SetState(state);
}
_userInterface.Open(actor.playerSession);
}
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(BodyScannerUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) { }
/// <summary>
/// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
/// </summary>
private BodyScannerInterfaceState InterfaceState(BodyTemplate template, IReadOnlyDictionary<string, BodyPart> bodyParts)
{
var partsData = new Dictionary<string, BodyScannerBodyPartData>();
foreach (var (slotName, part) in bodyParts)
{
var mechanismData = new List<BodyScannerMechanismData>();
foreach (var mechanism in part.Mechanisms)
{
mechanismData.Add(new BodyScannerMechanismData(mechanism.Name, mechanism.Description,
mechanism.RSIPath,
mechanism.RSIState, mechanism.MaxDurability, mechanism.CurrentDurability));
}
partsData.Add(slotName,
new BodyScannerBodyPartData(part.Name, part.RSIPath, part.RSIState, part.MaxDurability,
part.CurrentDurability, mechanismData));
}
var templateData = new BodyScannerTemplateData(template.Name, template.Slots);
return new BodyScannerInterfaceState(partsData, templateData);
}
}
}

View File

@@ -0,0 +1,83 @@
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Server.Interfaces;
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Circulatory
{
[RegisterComponent]
public class BloodstreamComponent : Component, IGasMixtureHolder
{
public override string Name => "Bloodstream";
/// <summary>
/// Max volume of internal solution storage
/// </summary>
[ViewVariables] private ReagentUnit _initialMaxVolume;
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables] private SolutionComponent _internalSolution;
/// <summary>
/// Empty volume of internal solution
/// </summary>
[ViewVariables] public ReagentUnit EmptyVolume => _internalSolution.EmptyVolume;
[ViewVariables] public GasMixture Air { get; set; } = new GasMixture(6);
[ViewVariables] public SolutionComponent Solution => _internalSolution;
public override void Initialize()
{
base.Initialize();
_internalSolution = Owner.EnsureComponent<SolutionComponent>();
_internalSolution.MaxVolume = _initialMaxVolume;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(250));
}
/// <summary>
/// Attempt to transfer provided solution to internal solution.
/// Only supports complete transfers
/// </summary>
/// <param name="solution">Solution to be transferred</param>
/// <returns>Whether or not transfer was a success</returns>
public bool TryTransferSolution(Solution solution)
{
// For now doesn't support partial transfers
if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
{
return false;
}
_internalSolution.TryAddSolution(solution, false, true);
return true;
}
public void PumpToxins(GasMixture into, float pressure)
{
if (!Owner.TryGetComponent(out MetabolismComponent metabolism))
{
Air.PumpGasTo(into, pressure);
return;
}
var toxins = metabolism.Clean(this);
toxins.PumpGasTo(into, pressure);
Air.Merge(toxins);
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
@@ -11,7 +11,7 @@ using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Nutrition
namespace Content.Server.GameObjects.Components.Body.Digestive
{
/// <summary>
/// Where reagents go when ingested. Tracks ingested reagents over time, and
@@ -25,7 +25,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
#pragma warning restore 649
/// <summary>
/// Max volume of internal solution storage
/// Max volume of internal solution storage
/// </summary>
public ReagentUnit MaxVolume
{
@@ -34,33 +34,29 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
/// <summary>
/// Internal solution storage
/// Internal solution storage
/// </summary>
[ViewVariables]
private SolutionComponent _stomachContents;
/// <summary>
/// Initial internal solution storage volume
/// Initial internal solution storage volume
/// </summary>
[ViewVariables]
private ReagentUnit _initialMaxVolume;
/// <summary>
/// Time in seconds between reagents being ingested and them being transferred to <see cref="BloodstreamComponent"/>
/// Time in seconds between reagents being ingested and them being transferred
/// to <see cref="BloodstreamComponent"/>
/// </summary>
[ViewVariables]
private float _digestionDelay;
/// <summary>
/// Used to track how long each reagent has been in the stomach
/// Used to track how long each reagent has been in the stomach
/// </summary>
private readonly List<ReagentDelta> _reagentDeltas = new List<ReagentDelta>();
/// <summary>
/// Reference to bloodstream where digested reagents are transferred to
/// </summary>
private BloodstreamComponent _bloodstream;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
@@ -70,14 +66,10 @@ namespace Content.Server.GameObjects.Components.Nutrition
protected override void Startup()
{
base.Startup();
_stomachContents = Owner.GetComponent<SolutionComponent>();
_stomachContents.MaxVolume = _initialMaxVolume;
if (!Owner.TryGetComponent<BloodstreamComponent>(out _bloodstream))
{
Logger.Warning(_localizationManager.GetString(
"StomachComponent entity does not have a BloodstreamComponent, which is required for it to function. Owner entity name: {0}",
Owner.Name));
}
}
public bool TryTransferSolution(Solution solution)
@@ -88,9 +80,9 @@ namespace Content.Server.GameObjects.Components.Nutrition
return false;
}
//Add solution to _stomachContents
// Add solution to _stomachContents
_stomachContents.TryAddSolution(solution, false, true);
//Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
// Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
@@ -100,23 +92,26 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
/// <summary>
/// Updates digestion status of ingested reagents. Once reagents surpass _digestionDelay
/// they are moved to the bloodstream, where they are then metabolized.
/// Updates digestion status of ingested reagents.
/// Once reagents surpass _digestionDelay they are moved to the bloodstream,
/// where they are then metabolized.
/// </summary>
/// <param name="tickTime">The time since the last update in seconds.</param>
public void OnUpdate(float tickTime)
/// <param name="frameTime">The time since the last update in seconds.</param>
public void Update(float frameTime)
{
if (_bloodstream == null)
if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
{
return;
}
//Add reagents ready for transfer to bloodstream to transferSolution
// Add reagents ready for transfer to bloodstream to transferSolution
var transferSolution = new Solution();
foreach (var delta in _reagentDeltas.ToList()) //Use ToList here to remove entries while iterating
// Use ToList here to remove entries while iterating
foreach (var delta in _reagentDeltas.ToList())
{
//Increment lifetime of reagents
delta.Increment(tickTime);
delta.Increment(frameTime);
if (delta.Lifetime > _digestionDelay)
{
_stomachContents.TryRemoveReagent(delta.ReagentId, delta.Quantity);
@@ -124,12 +119,13 @@ namespace Content.Server.GameObjects.Components.Nutrition
_reagentDeltas.Remove(delta);
}
}
//Transfer digested reagents to bloodstream
_bloodstream.TryTransferSolution(transferSolution);
// Transfer digested reagents to bloodstream
bloodstream.TryTransferSolution(transferSolution);
}
/// <summary>
/// Used to track quantity changes when ingesting & digesting reagents
/// Used to track quantity changes when ingesting & digesting reagents
/// </summary>
private class ReagentDelta
{

View File

@@ -1,10 +1,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Content.Shared.Health.BodySystem;
using Content.Shared.Health.BodySystem.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Server.Body;
using Content.Shared.Body.Surgery;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.Player;
@@ -14,98 +13,114 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
namespace Content.Server.Health.BodySystem.BodyPart
namespace Content.Server.GameObjects.Components.Body
{
/// <summary>
/// Component representing a dropped, tangible <see cref="BodyPart"/> entity.
/// Component representing a dropped, tangible <see cref="BodyPart"/> entity.
/// </summary>
[RegisterComponent]
public class DroppedBodyPartComponent : Component, IAfterInteract, IBodyPartContainer
{
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
#pragma warning restore 649
public sealed override string Name => "DroppedBodyPart";
[ViewVariables]
public BodyPart ContainedBodyPart { get; set; }
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private BodyManagerComponent _bodyManagerComponentCache;
private int _idHash;
private IEntity _performerCache;
private BoundUserInterface _userInterface;
private Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private IEntity _performerCache;
private BodyManagerComponent _bodyManagerComponentCache;
private int _idHash = 0;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>().GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
public sealed override string Name => "DroppedBodyPart";
public void TransferBodyPartData(BodyPart data)
{
ContainedBodyPart = data;
Owner.Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(ContainedBodyPart.Name);
if (Owner.TryGetComponent<SpriteComponent>(out SpriteComponent component))
{
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
}
}
[ViewVariables] public BodyPart ContainedBodyPart { get; private set; }
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_performerCache = null;
_bodyManagerComponentCache = null;
if (eventArgs.Target.TryGetComponent<BodyManagerComponent>(out BodyManagerComponent bodyManager))
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent bodyManager))
{
SendBodySlotListToUser(eventArgs, bodyManager);
}
}
private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
public override void Initialize()
{
var toSend = new Dictionary<string, int>(); //Create dictionary to send to client (text to be shown : data sent back if selected)
base.Initialize();
//Here we are trying to grab a list of all empty BodySlots adjancent to an existing BodyPart that can be attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
List<string> unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList();
foreach (string slot in unoccupiedSlots)
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
public void TransferBodyPartData(BodyPart data)
{
ContainedBodyPart = data;
Owner.Name = Loc.GetString(ContainedBodyPart.Name);
if (Owner.TryGetComponent(out SpriteComponent component))
{
if (bodyManager.TryGetSlotType(slot, out BodyPartType typeResult) && typeResult == ContainedBodyPart.PartType)
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
if (data.RSIColor.HasValue)
{
if (bodyManager.TryGetBodyPartConnections(slot, out List<BodyPart> bodypartResult))
{
foreach (BodyPart connectedPart in bodypartResult)
{
if (connectedPart.CanAttachBodyPart(ContainedBodyPart))
{
_optionsCache.Add(_idHash, slot);
toSend.Add(slot, _idHash++);
}
}
}
component.LayerSetColor(0, data.RSIColor.Value);
}
}
}
private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
{
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
// Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be
// attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
var unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList();
foreach (var slot in unoccupiedSlots)
{
if (!bodyManager.TryGetSlotType(slot, out var typeResult) ||
typeResult != ContainedBodyPart.PartType ||
!bodyManager.TryGetBodyPartConnections(slot, out var parts))
{
continue;
}
foreach (var connectedPart in parts)
{
if (!connectedPart.CanAttachBodyPart(ContainedBodyPart))
{
continue;
}
_optionsCache.Add(_idHash, slot);
toSend.Add(slot, _idHash++);
}
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent<BasicActorComponent>().playerSession);
UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession, toSend);
UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
toSend);
_performerCache = eventArgs.User;
_bodyManagerComponentCache = bodyManager;
}
else //If surgery cannot be performed, show message saying so.
else // If surgery cannot be performed, show message saying so.
{
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You see no way to install {0:theName}.", Owner));
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
Loc.GetString("You see no way to install {0:theName}.", Owner));
}
}
@@ -115,47 +130,50 @@ namespace Content.Server.Health.BodySystem.BodyPart
private void HandleReceiveBodyPartSlot(int key)
{
CloseSurgeryUI(_performerCache.GetComponent<BasicActorComponent>().playerSession);
//TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out object targetObject))
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject))
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
}
string target = targetObject as string;
if (!_bodyManagerComponentCache.InstallDroppedBodyPart(this, target))
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You can't attach it!"));
}
else
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You attach {0:theName}.", ContainedBodyPart));
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache,
Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
}
var target = targetObject as string;
_sharedNotifyManager.PopupMessage(
_bodyManagerComponentCache.Owner,
_performerCache,
!_bodyManagerComponentCache.InstallDroppedBodyPart(this, target)
? Loc.GetString("You can't attach it!")
: Loc.GetString("You attach {0:theName}.", ContainedBodyPart));
}
public void OpenSurgeryUI(IPlayerSession session)
private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
public void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
private void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
{
_userInterface.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session);
}
public void CloseSurgeryUI(IPlayerSession session)
private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
public void CloseAllSurgeryUIs()
private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSlotSurgeryUIMessage msg:
HandleReceiveBodyPartSlot(msg.SelectedOptionID);
HandleReceiveBodyPartSlot(msg.SelectedOptionId);
break;
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Content.Server.Health.BodySystem.BodyPart;
using Content.Shared.Health.BodySystem.Mechanism;
using Content.Shared.Health.BodySystem.Surgery;
using Content.Server.Body;
using Content.Server.Body.Mechanisms;
using Content.Shared.Body.Mechanism;
using Content.Shared.Body.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
@@ -18,15 +18,14 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.Health.BodySystem.Mechanism {
namespace Content.Server.GameObjects.Components.Body
{
/// <summary>
/// Component representing a dropped, tangible <see cref="Mechanism"/> entity.
/// Component representing a dropped, tangible <see cref="Mechanism"/> entity.
/// </summary>
[RegisterComponent]
public class DroppedMechanismComponent : Component, IAfterInteract
{
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
[Dependency] private IPrototypeManager _prototypeManager;
@@ -34,98 +33,120 @@ namespace Content.Server.Health.BodySystem.Mechanism {
public sealed override string Name => "DroppedMechanism";
[ViewVariables]
public Mechanism ContainedMechanism { get; private set; }
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private BodyManagerComponent _bodyManagerComponentCache;
private int _idHash;
private IEntity _performerCache;
private BoundUserInterface _userInterface;
private Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private IEntity _performerCache;
private BodyManagerComponent _bodyManagerComponentCache;
private int _idHash = 0;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>().GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
public void InitializeDroppedMechanism(Mechanism data)
{
ContainedMechanism = data;
Owner.Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(ContainedMechanism.Name);
if (Owner.TryGetComponent<SpriteComponent>(out SpriteComponent component))
{
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
}
}
[ViewVariables] public Mechanism ContainedMechanism { get; private set; }
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_performerCache = null;
_bodyManagerComponentCache = null;
if (eventArgs.Target.TryGetComponent<BodyManagerComponent>(out BodyManagerComponent bodyManager))
if (eventArgs.Target.TryGetComponent<BodyManagerComponent>(out var bodyManager))
{
SendBodyPartListToUser(eventArgs, bodyManager);
}
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out DroppedBodyPartComponent droppedBodyPart))
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out var droppedBodyPart))
{
if (droppedBodyPart.ContainedBodyPart == null)
{
Logger.Debug("Installing a mechanism was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
Logger.Debug(
"Installing a mechanism was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
throw new InvalidOperationException("A DroppedBodyPartComponent exists without a BodyPart in it!");
}
if (!droppedBodyPart.ContainedBodyPart.TryInstallDroppedMechanism(this))
{
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You can't fit it in!"));
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
Loc.GetString("You can't fit it in!"));
}
}
}
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
public void InitializeDroppedMechanism(Mechanism data)
{
ContainedMechanism = data;
Owner.Name = Loc.GetString(ContainedMechanism.Name);
if (Owner.TryGetComponent(out SpriteComponent component))
{
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
}
}
public override void ExposeData(ObjectSerializer serializer)
{
//This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes
//In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from the Mechanism prototypes
string debugLoadMechanismData = "";
// This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes
// In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from
// the Mechanism prototypes
var debugLoadMechanismData = "";
base.ExposeData(serializer);
serializer.DataField(ref debugLoadMechanismData, "debugLoadMechanismData", "");
if (serializer.Reading && debugLoadMechanismData != "")
{
_prototypeManager.TryIndex(debugLoadMechanismData, out MechanismPrototype data);
InitializeDroppedMechanism(new Mechanism(data));
var mechanism = new Mechanism(data);
mechanism.EnsureInitialize();
InitializeDroppedMechanism(mechanism);
}
}
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
{
var toSend = new Dictionary<string, int>(); //Create dictionary to send to client (text to be shown : data sent back if selected)
foreach (var (key, value) in bodyManager.PartDictionary)
{ //For each limb in the target, add it to our cache if it is a valid option.
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
foreach (var (key, value) in bodyManager.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (value.CanInstallMechanism(ContainedMechanism))
{
_optionsCache.Add(_idHash, value);
toSend.Add(key + ": " + value.Name, _idHash++);
}
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent<BasicActorComponent>().playerSession);
UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession, toSend);
UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
toSend);
_performerCache = eventArgs.User;
_bodyManagerComponentCache = bodyManager;
}
else //If surgery cannot be performed, show message saying so.
else // If surgery cannot be performed, show message saying so.
{
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You see no way to install the {0}.", Owner.Name));
_sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
Loc.GetString("You see no way to install the {0}.", Owner.Name));
}
}
@@ -135,52 +156,55 @@ namespace Content.Server.Health.BodySystem.Mechanism {
private void HandleReceiveBodyPart(int key)
{
CloseSurgeryUI(_performerCache.GetComponent<BasicActorComponent>().playerSession);
//TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out object targetObject))
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject))
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
}
BodyPart.BodyPart target = targetObject as BodyPart.BodyPart;
if (!target.TryInstallDroppedMechanism(this))
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You can't fit it in!"));
}
else
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You jam the {1} inside {0:them}.", _performerCache, ContainedMechanism.Name));
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache,
Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
return;
}
var target = targetObject as BodyPart;
_sharedNotifyManager.PopupMessage(
_bodyManagerComponentCache.Owner,
_performerCache,
!target.TryInstallDroppedMechanism(this)
? Loc.GetString("You can't fit it in!")
: Loc.GetString("You jam the {1} inside {0:them}.", _performerCache, ContainedMechanism.Name));
// TODO: {1:theName}
}
public void OpenSurgeryUI(IPlayerSession session)
private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
public void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
{
_userInterface.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
public void CloseSurgeryUI(IPlayerSession session)
private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
public void CloseAllSurgeryUIs()
private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
HandleReceiveBodyPart(msg.SelectedOptionID);
HandleReceiveBodyPart(msg.SelectedOptionId);
break;
}
}
}
}

View File

@@ -0,0 +1,127 @@
using System;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.Interfaces;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Respiratory
{
[RegisterComponent]
public class LungComponent : Component, IGasMixtureHolder
{
public override string Name => "Lung";
private float _accumulatedFrameTime;
/// <summary>
/// The pressure that this lung exerts on the air around it
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] private float Pressure { get; set; }
[ViewVariables] public GasMixture Air { get; set; } = new GasMixture();
[ViewVariables] public LungStatus Status { get; set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"volume",
6,
vol => Air.Volume = vol,
() => Air.Volume);
serializer.DataField(this, l => l.Pressure, "pressure", 100);
}
public void Update(float frameTime)
{
if (Status == LungStatus.None)
{
Status = LungStatus.Inhaling;
}
_accumulatedFrameTime += Status switch
{
LungStatus.Inhaling => frameTime,
LungStatus.Exhaling => -frameTime,
_ => throw new ArgumentOutOfRangeException()
};
var absoluteTime = Math.Abs(_accumulatedFrameTime);
if (absoluteTime < 2)
{
return;
}
switch (Status)
{
case LungStatus.Inhaling:
Inhale(absoluteTime);
Status = LungStatus.Exhaling;
break;
case LungStatus.Exhaling:
Exhale(absoluteTime);
Status = LungStatus.Inhaling;
break;
default:
throw new ArgumentOutOfRangeException();
}
_accumulatedFrameTime = absoluteTime - 2;
}
public void Inhale(float frameTime)
{
if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
{
return;
}
if (!Owner.Transform.GridPosition.TryGetTileAir(out var tileAir))
{
return;
}
var amount = Atmospherics.BreathPercentage * frameTime;
var volumeRatio = amount / tileAir.Volume;
var temp = tileAir.RemoveRatio(volumeRatio);
temp.PumpGasTo(Air, Pressure);
Air.PumpGasTo(bloodstream.Air, Pressure);
tileAir.Merge(temp);
}
public void Exhale(float frameTime)
{
if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
{
return;
}
if (!Owner.Transform.GridPosition.TryGetTileAir(out var tileAir))
{
return;
}
bloodstream.PumpToxins(Air, Pressure);
var amount = Atmospherics.BreathPercentage * frameTime;
var volumeRatio = amount / tileAir.Volume;
var temp = tileAir.RemoveRatio(volumeRatio);
temp.PumpGasTo(tileAir, Pressure);
Air.Merge(temp);
}
}
public enum LungStatus
{
None = 0,
Inhaling,
Exhaling
}
}

View File

@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using Content.Server.Health.BodySystem.BodyPart;
using Content.Server.Health.BodySystem.Mechanism;
using Content.Server.Body;
using Content.Server.Body.Mechanisms;
using Content.Server.Body.Surgery;
using Content.Shared.Body.Surgery;
using Content.Shared.GameObjects;
using Content.Shared.Health.BodySystem;
using Content.Shared.Health.BodySystem.Surgery;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
@@ -17,110 +19,136 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
namespace Content.Server.Health.BodySystem.Surgery.Surgeon
namespace Content.Server.GameObjects.Components.Body
{
//TODO: add checks to close UI if user walks too far away from tool or target.
// TODO: add checks to close UI if user walks too far away from tool or target.
/// <summary>
/// Server-side component representing a generic tool capable of performing surgery. For instance, the scalpel.
/// Server-side component representing a generic tool capable of performing surgery.
/// For instance, the scalpel.
/// </summary>
[RegisterComponent]
public class SurgeryToolComponent : Component, ISurgeon, IAfterInteract
{
public override string Name => "SurgeryTool";
public override uint? NetID => ContentNetIDs.SURGERY;
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
#pragma warning restore 649
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
public override string Name => "SurgeryTool";
public override uint? NetID => ContentNetIDs.SURGERY;
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private float _baseOperateTime;
private SurgeryType _surgeryType;
private HashSet<IPlayerSession> _subscribedSessions = new HashSet<IPlayerSession>();
private BoundUserInterface _userInterface;
private Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
private IEntity _performerCache;
private BodyManagerComponent _bodyManagerComponentCache;
private ISurgeon.MechanismRequestCallback _callbackCache;
private int _idHash = 0;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>().GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
private ISurgeon.MechanismRequestCallback _callbackCache;
private int _idHash;
private IEntity _performerCache;
private SurgeryType _surgeryType;
private BoundUserInterface _userInterface;
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
}
if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
{
return;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_performerCache = null;
_bodyManagerComponentCache = null;
_callbackCache = null;
if (eventArgs.Target.TryGetComponent<BodyManagerComponent>(out BodyManagerComponent bodyManager)) //Attempt surgery on a BodyManagerComponent by sending a list of operatable BodyParts to the client to choose from
// Attempt surgery on a BodyManagerComponent by sending a list of operable BodyParts to the client to choose from
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent body))
{
var toSend = new Dictionary<string, int>(); //Create dictionary to send to client (text to be shown : data sent back if selected)
foreach (var(key, value) in bodyManager.PartDictionary) { //For each limb in the target, add it to our cache if it is a valid option.
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
foreach (var (key, value) in body.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (value.SurgeryCheck(_surgeryType))
{
_optionsCache.Add(_idHash, value);
toSend.Add(key + ": " + value.Name, _idHash++);
}
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent<BasicActorComponent>().playerSession);
UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession, toSend);
_performerCache = eventArgs.User; //Also, cache the data.
_bodyManagerComponentCache = bodyManager;
OpenSurgeryUI(actor.playerSession);
UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend);
_performerCache = eventArgs.User; // Also, cache the data.
_bodyManagerComponentCache = body;
}
else //If surgery cannot be performed, show message saying so.
else // If surgery cannot be performed, show message saying so.
{
SendNoUsefulWayToUsePopup();
}
}
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out DroppedBodyPartComponent droppedBodyPart)) //Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out var droppedBodyPart))
{
// Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
_performerCache = eventArgs.User;
if (droppedBodyPart.ContainedBodyPart == null) //Throw error if the DroppedBodyPart has no data in it.
if (droppedBodyPart.ContainedBodyPart == null)
{
Logger.Debug("Surgery was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
// Throw error if the DroppedBodyPart has no data in it.
Logger.Debug(
"Surgery was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
throw new InvalidOperationException("A DroppedBodyPartComponent exists without a BodyPart in it!");
}
if (droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType)) //If surgery can be performed...
{
if (!droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this, eventArgs.User)) //...do the surgery.
{
Logger.Debug("Error when trying to perform surgery on bodypart " + eventArgs.User.Name + "!"); //Log error if the surgery fails somehow.
throw new InvalidOperationException();
}
}
else //If surgery cannot be performed, show message saying so.
// If surgery can be performed...
if (!droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType))
{
SendNoUsefulWayToUsePopup();
return;
}
//...do the surgery.
if (droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this,
eventArgs.User))
{
return;
}
// Log error if the surgery fails somehow.
Logger.Debug($"Error when trying to perform surgery on ${nameof(BodyPart)} {eventArgs.User.Name}");
throw new InvalidOperationException();
}
}
public void RequestMechanism(List<Mechanism.Mechanism> options, ISurgeon.MechanismRequestCallback callback)
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
public void RequestMechanism(IEnumerable<Mechanism> options, ISurgeon.MechanismRequestCallback callback)
{
var toSend = new Dictionary<string, int> ();
foreach (Mechanism.Mechanism mechanism in options)
var toSend = new Dictionary<string, int>();
foreach (var mechanism in options)
{
_optionsCache.Add(_idHash, mechanism);
toSend.Add(mechanism.Name, _idHash++);
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(_performerCache.GetComponent<BasicActorComponent>().playerSession);
UpdateSurgeryUIMechanismRequest(_performerCache.GetComponent<BasicActorComponent>().playerSession, toSend);
UpdateSurgeryUIMechanismRequest(_performerCache.GetComponent<BasicActorComponent>().playerSession,
toSend);
_callbackCache = callback;
}
else
@@ -130,95 +158,112 @@ namespace Content.Server.Health.BodySystem.Surgery.Surgeon
}
}
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(GenericSurgeryUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
public void OpenSurgeryUI(IPlayerSession session)
private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
public void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
{
_userInterface.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
public void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary<string, int> options)
private void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary<string, int> options)
{
_userInterface.SendMessage(new RequestMechanismSurgeryUIMessage(options), session);
}
public void CloseSurgeryUI(IPlayerSession session)
private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
public void CloseAllSurgeryUIs()
private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
HandleReceiveBodyPart(msg.SelectedOptionID);
HandleReceiveBodyPart(msg.SelectedOptionId);
break;
case ReceiveMechanismSurgeryUIMessage msg:
HandleReceiveMechanism(msg.SelectedOptionID);
HandleReceiveMechanism(msg.SelectedOptionId);
break;
}
}
/// <summary>
/// Called after the client chooses from a list of possible <see cref="BodyPart">BodyParts</see> that can be operated on.
/// Called after the client chooses from a list of possible
/// <see cref="BodyPart"/> that can be operated on.
/// </summary>
private void HandleReceiveBodyPart(int key)
{
CloseSurgeryUI(_performerCache.GetComponent<BasicActorComponent>().playerSession);
//TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out object targetObject))
{
SendNoUsefulWayToUseAnymorePopup();
}
BodyPart.BodyPart target = targetObject as BodyPart.BodyPart;
if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache))
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject))
{
SendNoUsefulWayToUseAnymorePopup();
}
}
/// <summary>
/// Called after the client chooses from a list of possible <see cref="Mechanism">Mechanisms</see> to choose from.
/// </summary>
private void HandleReceiveMechanism(int key)
{
//TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out object targetObject))
var target = targetObject as BodyPart;
if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache))
{
SendNoUsefulWayToUseAnymorePopup();
}
Mechanism.Mechanism target = targetObject as Mechanism.Mechanism;
}
/// <summary>
/// Called after the client chooses from a list of possible
/// <see cref="Mechanism"/> to choose from.
/// </summary>
private void HandleReceiveMechanism(int key)
{
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject))
{
SendNoUsefulWayToUseAnymorePopup();
}
var target = targetObject as Mechanism;
CloseSurgeryUI(_performerCache.GetComponent<BasicActorComponent>().playerSession);
_callbackCache(target, _bodyManagerComponentCache, this, _performerCache);
}
private void SendNoUsefulWayToUsePopup()
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use {0:theName}.", Owner));
_sharedNotifyManager.PopupMessage(
_bodyManagerComponentCache.Owner,
_performerCache,
Loc.GetString("You see no useful way to use {0:theName}.", Owner));
}
private void SendNoUsefulWayToUseAnymorePopup()
{
_sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
_sharedNotifyManager.PopupMessage(
_bodyManagerComponentCache.Owner,
_performerCache,
Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _surgeryType, "surgeryType", SurgeryType.Incision);
serializer.DataField(ref _baseOperateTime, "baseOperateTime", 5);
}

View File

@@ -112,7 +112,7 @@ namespace Content.Server.GameObjects.Components.Buckle
/// </summary>
private void BuckleStatus()
{
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status))
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? status))
{
status.ChangeStatusEffectIcon(StatusEffect.Buckled,
Buckled
@@ -291,7 +291,7 @@ namespace Content.Server.GameObjects.Components.Buckle
return false;
}
if (Owner.TryGetComponent(out AppearanceComponent appearance))
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(BuckleVisuals.Buckled, true);
}
@@ -359,12 +359,12 @@ namespace Content.Server.GameObjects.Components.Buckle
Owner.Transform.WorldRotation = oldBuckledTo.Owner.Transform.WorldRotation;
}
if (Owner.TryGetComponent(out AppearanceComponent appearance))
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(BuckleVisuals.Buckled, false);
}
if (Owner.TryGetComponent(out StunnableComponent stunnable) && stunnable.KnockedDown)
if (Owner.TryGetComponent(out StunnableComponent? stunnable) && stunnable.KnockedDown)
{
StandingStateHelper.Down(Owner);
}
@@ -373,14 +373,14 @@ namespace Content.Server.GameObjects.Components.Buckle
StandingStateHelper.Standing(Owner);
}
if (Owner.TryGetComponent(out SpeciesComponent species))
if (Owner.TryGetComponent(out MobStateManagerComponent? stateManager))
{
species.CurrentDamageState.EnterState(Owner);
stateManager.CurrentMobState.EnterState(Owner);
}
BuckleStatus();
if (oldBuckledTo.Owner.TryGetComponent(out StrapComponent strap))
if (oldBuckledTo.Owner.TryGetComponent(out StrapComponent? strap))
{
strap.Remove(this);
_entitySystem.GetEntitySystem<AudioSystem>()
@@ -535,7 +535,7 @@ namespace Content.Server.GameObjects.Components.Buckle
_entityManager.EventBus.UnsubscribeEvents(this);
if (BuckledTo != null &&
BuckledTo.Owner.TryGetComponent(out StrapComponent strap))
BuckledTo.Owner.TryGetComponent(out StrapComponent? strap))
{
strap.Remove(this);
}
@@ -552,7 +552,7 @@ namespace Content.Server.GameObjects.Components.Buckle
if (BuckledTo != null &&
Owner.Transform.WorldRotation.GetCardinalDir() == Direction.North &&
BuckledTo.Owner.TryGetComponent(out SpriteComponent strapSprite))
BuckledTo.Owner.TryGetComponent(out SpriteComponent? strapSprite))
{
drawDepth = strapSprite.DrawDepth - 1;
}

View File

@@ -159,7 +159,7 @@ namespace Content.Server.GameObjects.Components.Cargo
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
@@ -376,7 +377,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
bool IInteractUsing.InteractUsing(InteractUsingEventArgs args)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent hands))
{

View File

@@ -1,5 +1,5 @@
using System;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Shared.Chemistry;
@@ -134,7 +134,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
else //Handle injecting into bloodstream
{
if (targetEntity.TryGetComponent<BloodstreamComponent>(out var bloodstream) && _toggleState == InjectorToggleMode.Inject)
if (targetEntity.TryGetComponent(out BloodstreamComponent bloodstream) &&
_toggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}

View File

@@ -1,4 +1,5 @@
using Content.Server.GameObjects.Components.Nutrition;
using Content.Server.GameObjects.Components.Body.Digestive;
using Content.Server.GameObjects.Components.Nutrition;
using Content.Server.GameObjects.Components.Utensil;
using Content.Server.Utility;
using Content.Shared.Chemistry;

View File

@@ -1,4 +1,5 @@
using Content.Server.Interfaces;
using System.Threading.Tasks;
using Content.Server.Interfaces;
using Content.Shared.Chemistry;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
@@ -50,7 +51,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// </summary>
/// <param name="eventArgs">Attack event args</param>
/// <returns></returns>
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
//Get target solution component
if (!Owner.TryGetComponent<SolutionComponent>(out var targetSolution))

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
@@ -290,7 +291,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
bool IInteractUsing.InteractUsing(InteractUsingEventArgs args)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent hands))
{

View File

@@ -27,7 +27,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// ECS component that manages a liquid solution of reagents.
/// </summary>
[RegisterComponent]
internal class SolutionComponent : SharedSolutionComponent, IExamine
public class SolutionComponent : SharedSolutionComponent, IExamine
{
#pragma warning disable 649
[Dependency] private readonly IPrototypeManager _prototypeManager;

View File

@@ -1,6 +1,7 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
@@ -59,7 +60,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
{
_state = value;
if (!Owner.TryGetComponent(out AppearanceComponent appearance))
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
@@ -92,7 +93,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
return false;
}
if (Owner.TryGetComponent(out PowerReceiverComponent receiver) &&
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return false;
@@ -113,7 +114,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
return false;
}
if (!entity.TryGetComponent(out ICollidableComponent collidable) ||
if (!entity.TryGetComponent(out ICollidableComponent? collidable) ||
collidable.Anchored)
{
return false;
@@ -154,7 +155,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
continue;
}
if (entity.TryGetComponent(out ICollidableComponent collidable))
if (entity.TryGetComponent(out ICollidableComponent? collidable))
{
var controller = collidable.EnsureController<ConveyedController>();
controller.Move(direction, _speed * frameTime);
@@ -162,10 +163,10 @@ namespace Content.Server.GameObjects.Components.Conveyor
}
}
private bool ToolUsed(IEntity user, ToolComponent tool)
private async Task<bool> ToolUsed(IEntity user, ToolComponent tool)
{
if (!Owner.HasComponent<ItemComponent>() &&
tool.UseTool(user, Owner, ToolQuality.Prying))
await tool.UseTool(user, Owner, 0.5f, ToolQuality.Prying))
{
State = ConveyorState.Loose;
@@ -224,7 +225,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
continue;
}
if (!@switch.TryGetComponent(out ConveyorSwitchComponent component))
if (!@switch.TryGetComponent(out ConveyorSwitchComponent? component))
{
continue;
}
@@ -244,17 +245,17 @@ namespace Content.Server.GameObjects.Components.Conveyor
Disconnect();
}
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (eventArgs.Using.TryGetComponent(out ConveyorSwitchComponent conveyorSwitch))
if (eventArgs.Using.TryGetComponent(out ConveyorSwitchComponent? conveyorSwitch))
{
conveyorSwitch.Connect(this, eventArgs.User);
return true;
}
if (eventArgs.Using.TryGetComponent(out ToolComponent tool))
if (eventArgs.Using.TryGetComponent(out ToolComponent? tool))
{
return ToolUsed(eventArgs.User, tool);
return await ToolUsed(eventArgs.User, tool);
}
return false;

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Conveyor;
using Content.Shared.Interfaces;
@@ -33,7 +34,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
{
_state = value;
if (Owner.TryGetComponent(out AppearanceComponent appearance))
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ConveyorVisuals.State, value);
}
@@ -144,7 +145,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
continue;
}
if (!conveyor.TryGetComponent(out ConveyorComponent component))
if (!conveyor.TryGetComponent(out ConveyorComponent? component))
{
continue;
}
@@ -171,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
continue;
}
if (!@switch.TryGetComponent(out ConveyorSwitchComponent component))
if (!@switch.TryGetComponent(out ConveyorSwitchComponent? component))
{
continue;
}
@@ -193,15 +194,15 @@ namespace Content.Server.GameObjects.Components.Conveyor
return NextState();
}
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (eventArgs.Using.TryGetComponent(out ConveyorComponent conveyor))
if (eventArgs.Using.TryGetComponent(out ConveyorComponent? conveyor))
{
Connect(conveyor, eventArgs.User);
return true;
}
if (eventArgs.Using.TryGetComponent(out ConveyorSwitchComponent otherSwitch))
if (eventArgs.Using.TryGetComponent(out ConveyorSwitchComponent? otherSwitch))
{
SyncWith(otherSwitch, eventArgs.User);
return true;

View File

@@ -1,39 +1,59 @@
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
// TODO: Repair needs to set CurrentDamageState to DamageState.Alive, but it doesn't exist... should be easy enough if it's just an interface you can slap on BreakableComponent
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage and sets it to a "broken state" after taking
/// enough damage.
/// </summary>
[RegisterComponent]
public class BreakableComponent : Component, IOnDamageBehavior, IExAct
[ComponentReference(typeof(IDamageableComponent))]
public class BreakableComponent : RuinableComponent, IExAct
{
#pragma warning disable 649
#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
#pragma warning restore 649
/// <inheritdoc />
public override string Name => "Breakable";
public DamageThreshold Threshold { get; private set; }
[Dependency] private readonly IRobustRandom _random;
#pragma warning restore 649
public DamageType damageType = DamageType.Total;
public int damageValue = 0;
public bool broken = false;
public override string Name => "Breakable";
private ActSystem _actSystem;
private DamageState _currentDamageState;
public override void ExposeData(ObjectSerializer serializer)
public override List<DamageState> SupportedDamageStates =>
new List<DamageState> {DamageState.Alive, DamageState.Dead};
public override DamageState CurrentDamageState => _currentDamageState;
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
base.ExposeData(serializer);
switch (eventArgs.Severity)
{
case ExplosionSeverity.Destruction:
PerformDestruction();
break;
case ExplosionSeverity.Heavy:
PerformDestruction();
break;
case ExplosionSeverity.Light:
if (_random.Prob(0.5f))
{
PerformDestruction();
}
serializer.DataField(ref damageValue, "thresholdvalue", 100);
serializer.DataField(ref damageType, "thresholdtype", DamageType.Total);
break;
}
}
public override void Initialize()
@@ -42,38 +62,21 @@ namespace Content.Server.GameObjects.Components.Damage
_actSystem = _entitySystemManager.GetEntitySystem<ActSystem>();
}
public List<DamageThreshold> GetAllDamageThresholds()
// Might want to move this down and have a more standardized method of revival
public void FixAllDamage()
{
Threshold = new DamageThreshold(damageType, damageValue, ThresholdType.Breakage);
return new List<DamageThreshold>() {Threshold};
Heal();
_currentDamageState = DamageState.Alive;
}
public void OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e)
protected override void DestructionBehavior()
{
if (e.Passed && e.DamageThreshold == Threshold && broken == false)
_actSystem.HandleBreakage(Owner);
if (!Owner.Deleted && DestroySound != string.Empty)
{
broken = true;
_actSystem.HandleBreakage(Owner);
var pos = Owner.Transform.GridPosition;
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
}
}
public void OnExplosion(ExplosionEventArgs eventArgs)
{
var prob = IoCManager.Resolve<IRobustRandom>();
switch (eventArgs.Severity)
{
case ExplosionSeverity.Destruction:
_actSystem.HandleBreakage(Owner);
break;
case ExplosionSeverity.Heavy:
_actSystem.HandleBreakage(Owner);
break;
case ExplosionSeverity.Light:
if(prob.Prob(0.4f))
_actSystem.HandleBreakage(Owner);
break;
}
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
@@ -23,7 +24,7 @@ namespace Content.Server.GameObjects.Components.Damage
public override string Name => "DamageOnHighSpeedImpact";
public DamageType Damage { get; set; } = DamageType.Brute;
public DamageType Damage { get; set; } = DamageType.Blunt;
public float MinimumSpeed { get; set; } = 20f;
public int BaseDamage { get; set; } = 5;
public float Factor { get; set; } = 0.75f;
@@ -38,7 +39,7 @@ namespace Content.Server.GameObjects.Components.Damage
{
base.ExposeData(serializer);
serializer.DataField(this, x => Damage, "damage", DamageType.Brute);
serializer.DataField(this, x => Damage, "damage", DamageType.Blunt);
serializer.DataField(this, x => MinimumSpeed, "minimumSpeed", 20f);
serializer.DataField(this, x => BaseDamage, "baseDamage", 5);
serializer.DataField(this, x => Factor, "factor", 1f);
@@ -51,7 +52,7 @@ namespace Content.Server.GameObjects.Components.Damage
public void CollideWith(IEntity collidedWith)
{
if (!Owner.TryGetComponent(out ICollidableComponent collidable) || !Owner.TryGetComponent(out DamageableComponent damageable)) return;
if (!Owner.TryGetComponent(out ICollidableComponent collidable) || !Owner.TryGetComponent(out IDamageableComponent damageable)) return;
var speed = collidable.LinearVelocity.Length;
@@ -70,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Damage
if (Owner.TryGetComponent(out StunnableComponent stun) && _robustRandom.Prob(StunChance))
stun.Stun(StunSeconds);
damageable.TakeDamage(Damage, damage, collidedWith, Owner);
damageable.ChangeDamage(Damage, damage, false, collidedWith);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
@@ -9,7 +10,7 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
[RegisterComponent]
class DamageOnToolInteractComponent : Component, IInteractUsing
public class DamageOnToolInteractComponent : Component, IInteractUsing
{
public override string Name => "DamageOnToolInteract";
@@ -29,10 +30,10 @@ namespace Content.Server.GameObjects.Components.Damage
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<DamageableComponent>();
Owner.EnsureComponent<DestructibleComponent>();
}
public bool InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
{
@@ -40,12 +41,12 @@ namespace Content.Server.GameObjects.Components.Damage
{
if (tool.HasQuality(ToolQuality.Welding) && toolQuality == ToolQuality.Welding)
{
if (eventArgs.Using.TryGetComponent<WelderComponent>(out WelderComponent welder))
{
if (eventArgs.Using.TryGetComponent(out WelderComponent welder))
{
if (welder.WelderLit) return CallDamage(eventArgs, tool);
}
}
break; //If the tool quality is welding and its not lit or its not actually a welder that can be lit then its pointless to continue.
}
}
if (tool.HasQuality(toolQuality)) return CallDamage(eventArgs, tool);
}
@@ -55,14 +56,17 @@ namespace Content.Server.GameObjects.Components.Damage
protected bool CallDamage(InteractUsingEventArgs eventArgs, ToolComponent tool)
{
if (eventArgs.Target.TryGetComponent<DamageableComponent>(out var damageable))
if (eventArgs.Target.TryGetComponent<DestructibleComponent>(out var damageable))
{
if(tool.HasQuality(ToolQuality.Welding)) damageable.TakeDamage(DamageType.Heat, Damage, eventArgs.Using, eventArgs.User);
else
damageable.TakeDamage(DamageType.Brute, Damage, eventArgs.Using, eventArgs.User);
damageable.ChangeDamage(tool.HasQuality(ToolQuality.Welding)
? DamageType.Heat
: DamageType.Blunt,
Damage, false, eventArgs.User);
return true;
}
return false;
return false;
}
}
}

View File

@@ -1,98 +0,0 @@
using System;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// Triggers an event when values rise above or drop below this threshold
/// </summary>
public struct DamageThreshold
{
public DamageType DamageType { get; }
public int Value { get; }
public ThresholdType ThresholdType { get; }
public DamageThreshold(DamageType damageType, int value, ThresholdType thresholdType)
{
DamageType = damageType;
Value = value;
ThresholdType = thresholdType;
}
public override bool Equals(Object obj)
{
return obj is DamageThreshold threshold && this == threshold;
}
public override int GetHashCode()
{
return DamageType.GetHashCode() ^ Value.GetHashCode();
}
public static bool operator ==(DamageThreshold x, DamageThreshold y)
{
return x.DamageType == y.DamageType && x.Value == y.Value;
}
public static bool operator !=(DamageThreshold x, DamageThreshold y)
{
return !(x == y);
}
}
public enum ThresholdType
{
None,
Destruction,
Death,
Critical,
HUDUpdate,
Breakage,
}
public class DamageThresholdPassedEventArgs : EventArgs
{
public DamageThreshold DamageThreshold { get; }
public bool Passed { get; }
public int ExcessDamage { get; }
public DamageThresholdPassedEventArgs(DamageThreshold threshold, bool passed, int excess)
{
DamageThreshold = threshold;
Passed = passed;
ExcessDamage = excess;
}
}
public class DamageEventArgs : EventArgs
{
/// <summary>
/// Type of damage.
/// </summary>
public DamageType Type { get; }
/// <summary>
/// Change in damage.
/// </summary>
public int Damage { get; }
/// <summary>
/// The entity that damaged this one.
/// Could be null.
/// </summary>
public IEntity Source { get; }
/// <summary>
/// The mob entity that damaged this one.
/// Could be null.
/// </summary>
public IEntity SourceMob { get; }
public DamageEventArgs(DamageType type, int damage, IEntity source, IEntity sourceMob)
{
Type = type;
Damage = damage;
Source = source;
SourceMob = sourceMob;
}
}
}

View File

@@ -1,212 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Interfaces.GameObjects;
using Content.Server.Interfaces.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Damage
{
//TODO: add support for component add/remove
/// <summary>
/// A component that handles receiving damage and healing,
/// as well as informing other components of it.
/// </summary>
[RegisterComponent]
public class DamageableComponent : SharedDamageableComponent, IDamageableComponent
{
/// <inheritdoc />
public override string Name => "Damageable";
/// <summary>
/// The resistance set of this object.
/// Affects receiving damage of various types.
/// </summary>
[ViewVariables]
public ResistanceSet Resistances { get; private set; }
[ViewVariables]
public IReadOnlyDictionary<DamageType, int> CurrentDamage => _currentDamage;
private Dictionary<DamageType, int> _currentDamage = new Dictionary<DamageType, int>();
[ViewVariables]
public Dictionary<DamageType, List<DamageThreshold>> Thresholds = new Dictionary<DamageType, List<DamageThreshold>>();
public event EventHandler<DamageThresholdPassedEventArgs> DamageThresholdPassed;
public event EventHandler<DamageEventArgs> Damaged;
public override ComponentState GetComponentState()
{
return new DamageComponentState(_currentDamage);
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => Resistances, "resistances", ResistanceSet.DefaultResistanceSet);
}
public bool IsDead()
{
var currentDamage = _currentDamage[DamageType.Total];
foreach (var threshold in Thresholds[DamageType.Total])
{
if (threshold.Value <= currentDamage)
{
if (threshold.ThresholdType != ThresholdType.Death) continue;
return true;
}
}
return false;
}
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
InitializeDamageType(DamageType.Total);
foreach (var damagebehavior in Owner.GetAllComponents<IOnDamageBehavior>())
{
AddThresholdsFrom(damagebehavior);
Damaged += damagebehavior.OnDamaged;
}
RecalculateComponentThresholds();
}
/// <inheritdoc />
public void TakeDamage(DamageType damageType, int amount, IEntity source = null, IEntity sourceMob = null)
{
if (damageType == DamageType.Total)
{
foreach (DamageType e in Enum.GetValues(typeof(DamageType)))
{
if (e == damageType) continue;
TakeDamage(e, amount, source, sourceMob);
}
return;
}
InitializeDamageType(damageType);
int oldValue = _currentDamage[damageType];
int oldTotalValue = -1;
if (amount == 0)
{
return;
}
amount = Resistances.CalculateDamage(damageType, amount);
_currentDamage[damageType] = Math.Max(0, _currentDamage[damageType] + amount);
UpdateForDamageType(damageType, oldValue);
Damaged?.Invoke(this, new DamageEventArgs(damageType, amount, source, sourceMob));
if (Resistances.AppliesToTotal(damageType))
{
oldTotalValue = _currentDamage[DamageType.Total];
_currentDamage[DamageType.Total] = Math.Max(0, _currentDamage[DamageType.Total] + amount);
UpdateForDamageType(DamageType.Total, oldTotalValue);
}
}
/// <inheritdoc />
public void TakeHealing(DamageType damageType, int amount, IEntity source = null, IEntity sourceMob = null)
{
if (damageType == DamageType.Total)
{
throw new ArgumentException("Cannot heal for DamageType.Total");
}
TakeDamage(damageType, -amount, source, sourceMob);
}
public void HealAllDamage()
{
var values = Enum.GetValues(typeof(DamageType)).Cast<DamageType>();
foreach (var damageType in values)
{
if (CurrentDamage.ContainsKey(damageType) && damageType != DamageType.Total)
{
TakeHealing(damageType, CurrentDamage[damageType]);
}
}
}
void UpdateForDamageType(DamageType damageType, int oldValue)
{
int change = _currentDamage[damageType] - oldValue;
if (change == 0)
{
return;
}
int changeSign = Math.Sign(change);
foreach (var threshold in Thresholds[damageType])
{
var value = threshold.Value;
if (((value * changeSign) > (oldValue * changeSign)) && ((value * changeSign) <= (_currentDamage[damageType] * changeSign)))
{
var excessDamage = change - value;
var typeOfDamage = damageType;
if (change - value < 0)
{
excessDamage = 0;
}
var args = new DamageThresholdPassedEventArgs(threshold, (changeSign > 0), excessDamage);
DamageThresholdPassed?.Invoke(this, args);
}
}
}
void RecalculateComponentThresholds()
{
foreach (IOnDamageBehavior onDamageBehaviorComponent in Owner.GetAllComponents<IOnDamageBehavior>())
{
AddThresholdsFrom(onDamageBehaviorComponent);
}
}
void AddThresholdsFrom(IOnDamageBehavior onDamageBehavior)
{
if (onDamageBehavior == null)
{
throw new ArgumentNullException(nameof(onDamageBehavior));
}
List<DamageThreshold> thresholds = onDamageBehavior.GetAllDamageThresholds();
if (thresholds == null)
return;
foreach (DamageThreshold threshold in thresholds)
{
if (!Thresholds[threshold.DamageType].Contains(threshold))
{
Thresholds[threshold.DamageType].Add(threshold);
}
}
DamageThresholdPassed += onDamageBehavior.OnDamageThresholdPassed;
}
void InitializeDamageType(DamageType damageType)
{
if (!_currentDamage.ContainsKey(damageType))
{
_currentDamage.Add(damageType, 0);
Thresholds.Add(damageType, new List<DamageThreshold>());
}
}
}
}

View File

@@ -1,114 +1,67 @@
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// Deletes the entity once a certain damage threshold has been reached.
/// When attached to an <see cref="IEntity"/>, allows it to take damage and deletes it after taking enough damage.
/// </summary>
[RegisterComponent]
public class DestructibleComponent : Component, IOnDamageBehavior, IDestroyAct, IExAct
[ComponentReference(typeof(IDamageableComponent))]
public class DestructibleComponent : RuinableComponent, IDestroyAct
{
#pragma warning disable 649
#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
#pragma warning restore 649
#pragma warning restore 649
protected ActSystem ActSystem;
/// <inheritdoc />
public override string Name => "Destructible";
/// <summary>
/// Damage threshold calculated from the values
/// given in the prototype declaration.
/// Entity spawned upon destruction.
/// </summary>
[ViewVariables]
public DamageThreshold Threshold { get; private set; }
public string SpawnOnDestroy { get; set; }
public DamageType damageType = DamageType.Total;
public int damageValue = 0;
public string spawnOnDestroy = "";
public string destroySound = "";
public bool destroyed = false;
ActSystem _actSystem;
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
if (!string.IsNullOrWhiteSpace(SpawnOnDestroy) && eventArgs.IsSpawnWreck)
{
Owner.EntityManager.SpawnEntity(SpawnOnDestroy, Owner.Transform.GridPosition);
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref damageValue, "thresholdvalue", 100);
serializer.DataField(ref damageType, "thresholdtype", DamageType.Total);
serializer.DataField(ref spawnOnDestroy, "spawnondestroy", "");
serializer.DataField(ref destroySound, "destroysound", "");
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnondestroy", string.Empty);
}
public override void Initialize()
{
base.Initialize();
_actSystem = _entitySystemManager.GetEntitySystem<ActSystem>();
ActSystem = _entitySystemManager.GetEntitySystem<ActSystem>();
}
/// <inheritdoc />
List<DamageThreshold> IOnDamageBehavior.GetAllDamageThresholds()
{
Threshold = new DamageThreshold(damageType, damageValue, ThresholdType.Destruction);
return new List<DamageThreshold>() { Threshold };
}
/// <inheritdoc />
void IOnDamageBehavior.OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e)
protected override void DestructionBehavior()
{
if (e.Passed && e.DamageThreshold == Threshold && destroyed == false)
if (!Owner.Deleted)
{
destroyed = true;
var pos = Owner.Transform.GridPosition;
_actSystem.HandleDestruction(Owner, true);
if(destroySound != string.Empty)
ActSystem.HandleDestruction(Owner,
true); //This will call IDestroyAct.OnDestroy on this component (and all other components on this entity)
if (DestroySound != string.Empty)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(destroySound, pos);
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
}
}
}
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
var prob = IoCManager.Resolve<IRobustRandom>();
switch (eventArgs.Severity)
{
case ExplosionSeverity.Destruction:
_actSystem.HandleDestruction(Owner, false);
break;
case ExplosionSeverity.Heavy:
var spawnWreckOnHeavy = prob.Prob(0.5f);
_actSystem.HandleDestruction(Owner, spawnWreckOnHeavy);
break;
case ExplosionSeverity.Light:
if (prob.Prob(0.4f))
_actSystem.HandleDestruction(Owner, true);
break;
}
}
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
if (!string.IsNullOrWhiteSpace(spawnOnDestroy) && eventArgs.IsSpawnWreck)
{
Owner.EntityManager.SpawnEntity(spawnOnDestroy, Owner.Transform.GridPosition);
}
}
}

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// Resistance set used by damageable objects.
/// For each damage type, has a coefficient, damage reduction and "included in total" value.
/// </summary>
public class ResistanceSet : IExposeData
{
public static ResistanceSet DefaultResistanceSet = new ResistanceSet();
[ViewVariables]
private readonly Dictionary<DamageType, ResistanceSetSettings> _resistances = new Dictionary<DamageType, ResistanceSetSettings>();
public ResistanceSet()
{
foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
{
_resistances[damageType] = new ResistanceSetSettings();
}
}
public void ExposeData(ObjectSerializer serializer)
{
foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
{
var resistanceName = damageType.ToString().ToLower();
serializer.DataReadFunction(resistanceName, new ResistanceSetSettings(), resistanceSetting =>
{
_resistances[damageType] = resistanceSetting;
});
}
}
/// <summary>
/// Adjusts input damage with the resistance set values.
/// </summary>
/// <param name="damageType">Type of the damage.</param>
/// <param name="amount">Incoming amount of the damage.</param>
/// <returns>Damage adjusted by the resistance set.</returns>
public int CalculateDamage(DamageType damageType, int amount)
{
if (amount > 0) //if it's damage, reduction applies
{
amount -= _resistances[damageType].DamageReduction;
if (amount <= 0)
return 0;
}
amount = (int)Math.Floor(amount * _resistances[damageType].Coefficient);
return amount;
}
public bool AppliesToTotal(DamageType damageType)
{
//Damage that goes straight to total (for whatever reason) never applies twice
return damageType != DamageType.Total && _resistances[damageType].AppliesToTotal;
}
/// <summary>
/// Settings for a specific damage type in a resistance set.
/// </summary>
public class ResistanceSetSettings : IExposeData
{
public float Coefficient { get; private set; } = 1;
public int DamageReduction { get; private set; } = 0;
public bool AppliesToTotal { get; private set; } = true;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => Coefficient, "coefficient", 1);
serializer.DataField(this, x => DamageReduction, "damageReduction", 0);
serializer.DataField(this, x => AppliesToTotal, "appliesToTotal", true);
}
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Damage
{
/// <summary>
/// When attached to an <see cref="IEntity"/>, allows it to take damage and
/// "ruins" or "destroys" it after enough damage is taken.
/// </summary>
[ComponentReference(typeof(IDamageableComponent))]
public abstract class RuinableComponent : DamageableComponent
{
private DamageState _currentDamageState;
/// <summary>
/// How much HP this component can sustain before triggering
/// <see cref="PerformDestruction"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int MaxHp { get; private set; }
/// <summary>
/// Sound played upon destruction.
/// </summary>
protected string DestroySound { get; private set; }
public override List<DamageState> SupportedDamageStates =>
new List<DamageState> {DamageState.Alive, DamageState.Dead};
public override DamageState CurrentDamageState => _currentDamageState;
public override void Initialize()
{
base.Initialize();
HealthChangedEvent += OnHealthChanged;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, ruinable => ruinable.MaxHp, "maxHP", 100);
serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty);
}
public override void OnRemove()
{
base.OnRemove();
HealthChangedEvent -= OnHealthChanged;
}
private void OnHealthChanged(HealthChangedEventArgs e)
{
if (CurrentDamageState != DamageState.Dead && TotalDamage >= MaxHp)
{
PerformDestruction();
}
}
/// <summary>
/// Destroys the Owner <see cref="IEntity"/>, setting
/// <see cref="IDamageableComponent.CurrentDamageState"/> to
/// <see cref="DamageState.Dead"/>
/// </summary>
protected void PerformDestruction()
{
_currentDamageState = DamageState.Dead;
if (!Owner.Deleted && DestroySound != string.Empty)
{
var pos = Owner.Transform.GridPosition;
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
}
DestructionBehavior();
}
protected abstract void DestructionBehavior();
}
}

View File

@@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.Components.Disposal
return;
}
if (!entity.TryGetComponent(out IDisposalTubeComponent tube))
if (!entity.TryGetComponent(out IDisposalTubeComponent? tube))
{
shell.SendText(player, Loc.GetString("Entity with uid {0} doesn't have a {1} component", id, nameof(IDisposalTubeComponent)));
return;

View File

@@ -1,10 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Body;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
@@ -41,6 +43,12 @@ namespace Content.Server.GameObjects.Components.Disposal
[ViewVariables]
public IDisposalTubeComponent? NextTube { get; set; }
/// <summary>
/// A list of tags attached to the content, used for sorting
/// </summary>
[ViewVariables]
public HashSet<string> Tags { get; set; } = new HashSet<string>();
private bool CanInsert(IEntity entity)
{
if (!_contents.CanInsert(entity))
@@ -48,8 +56,14 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (!entity.TryGetComponent(out ICollidableComponent? collidable) ||
!collidable.CanCollide)
{
return false;
}
return entity.HasComponent<ItemComponent>() ||
entity.HasComponent<SpeciesComponent>();
entity.HasComponent<IBodyManagerComponent>();
}
public bool TryInsert(IEntity entity)
@@ -59,6 +73,11 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (entity.TryGetComponent(out ICollidableComponent? collidable))
{
collidable.CanCollide = false;
}
return true;
}
@@ -86,6 +105,11 @@ namespace Content.Server.GameObjects.Components.Disposal
foreach (var entity in _contents.ContainedEntities.ToArray())
{
if (entity.TryGetComponent(out ICollidableComponent? collidable))
{
collidable.CanCollide = true;
}
_contents.ForceRemove(entity);
if (entity.Transform.Parent == Owner.Transform)

View File

@@ -0,0 +1,179 @@
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using System;
using System.Collections.Generic;
using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalRouterComponent;
namespace Content.Server.GameObjects.Components.Disposal
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalRouterComponent : DisposalJunctionComponent, IActivate
{
#pragma warning disable 649
[Dependency] private readonly IServerNotifyManager _notifyManager;
#pragma warning restore 649
public override string Name => "DisposalRouter";
[ViewVariables]
private BoundUserInterface _userInterface;
[ViewVariables]
private HashSet<string> _tags;
[ViewVariables]
public bool Anchored =>
!Owner.TryGetComponent(out CollidableComponent collidable) ||
collidable.Anchored;
public override Direction NextDirection(DisposalHolderComponent holder)
{
var directions = ConnectableDirections();
if (holder.Tags.Overlaps(_tags))
{
return directions[1];
}
return Owner.Transform.LocalRotation.GetDir();
}
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(DisposalRouterUiKey.Key);
_userInterface.OnReceiveMessage += OnUiReceiveMessage;
_tags = new HashSet<string>();
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
var msg = (UiActionMessage) obj.Message;
if (!PlayerCanUseDisposalTagger(obj.Session.AttachedEntity))
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tags))
{
_tags.Clear();
foreach (var tag in msg.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
_tags.Add(tag.Trim());
ClickSound();
}
}
}
/// <summary>
/// Checks whether the player entity is able to use the configuration interface of the pipe tagger.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the configuration interface, and false if it cannot.</returns>
private bool PlayerCanUseDisposalTagger(IEntity playerEntity)
{
//Need player entity to check if they are still able to use the configuration interface
if (playerEntity == null)
return false;
if (!Anchored)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedDisposalRouterComponent.DisposalRouterBoundUserInterfaceState"/></returns>
private DisposalRouterUserInterfaceState GetUserInterfaceState()
{
if(_tags == null || _tags.Count <= 0)
{
return new DisposalRouterUserInterfaceState("");
}
var taglist = new System.Text.StringBuilder();
foreach (var tag in _tags)
{
taglist.Append(tag);
taglist.Append(", ");
}
taglist.Remove(taglist.Length - 2, 2);
return new DisposalRouterUserInterfaceState(taglist.ToString());
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
_userInterface.SetState(state);
}
private void ClickSound()
{
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out IActorComponent actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent hands))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UpdateUserInterface();
_userInterface.Open(actor.playerSession);
}
}
public override void OnRemove()
{
_userInterface.CloseAll();
base.OnRemove();
}
}
}

View File

@@ -0,0 +1,150 @@
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalTaggerComponent;
namespace Content.Server.GameObjects.Components.Disposal
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IDisposalTubeComponent))]
public class DisposalTaggerComponent : DisposalTransitComponent, IActivate
{
#pragma warning disable 649
[Dependency] private readonly IServerNotifyManager _notifyManager;
#pragma warning restore 649
public override string Name => "DisposalTagger";
[ViewVariables]
private BoundUserInterface _userInterface;
[ViewVariables(VVAccess.ReadWrite)]
private string _tag = "";
[ViewVariables]
public bool Anchored =>
!Owner.TryGetComponent(out CollidableComponent collidable) ||
collidable.Anchored;
public override Direction NextDirection(DisposalHolderComponent holder)
{
holder.Tags.Add(_tag);
return base.NextDirection(holder);
}
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(DisposalTaggerUiKey.Key);
_userInterface.OnReceiveMessage += OnUiReceiveMessage;
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
var msg = (UiActionMessage) obj.Message;
if (!PlayerCanUseDisposalTagger(obj.Session.AttachedEntity))
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tag))
{
_tag = msg.Tag;
ClickSound();
}
}
/// <summary>
/// Checks whether the player entity is able to use the configuration interface of the pipe tagger.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the configuration interface, and false if it cannot.</returns>
private bool PlayerCanUseDisposalTagger(IEntity playerEntity)
{
//Need player entity to check if they are still able to use the configuration interface
if (playerEntity == null)
return false;
if (!Anchored)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedDisposalTaggerComponent.DisposalTaggerBoundUserInterfaceState"/></returns>
private DisposalTaggerUserInterfaceState GetUserInterfaceState()
{
return new DisposalTaggerUserInterfaceState(_tag);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
_userInterface.SetState(state);
}
private void ClickSound()
{
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out IActorComponent actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent hands))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User,
Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UpdateUserInterface();
_userInterface.Open(actor.playerSession);
}
}
public override void OnRemove()
{
base.OnRemove();
_userInterface.CloseAll();
}
}
}

View File

@@ -1,9 +1,9 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Robust.Server.Console;
using Robust.Server.GameObjects;
@@ -44,7 +44,7 @@ namespace Content.Server.GameObjects.Components.Disposal
[ViewVariables]
private bool Anchored =>
!Owner.TryGetComponent(out CollidableComponent collidable) ||
!Owner.TryGetComponent(out CollidableComponent? collidable) ||
collidable.Anchored;
/// <summary>
@@ -71,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Disposal
var snapGrid = Owner.GetComponent<SnapGridComponent>();
var tube = snapGrid
.GetInDir(nextDirection)
.Select(x => x.TryGetComponent(out IDisposalTubeComponent c) ? c : null)
.Select(x => x.TryGetComponent(out IDisposalTubeComponent? c) ? c : null)
.FirstOrDefault(x => x != null && x != this);
if (tube == null)
@@ -153,7 +153,7 @@ namespace Content.Server.GameObjects.Components.Disposal
foreach (var entity in Contents.ContainedEntities.ToArray())
{
if (!entity.TryGetComponent(out DisposalHolderComponent holder))
if (!entity.TryGetComponent(out DisposalHolderComponent? holder))
{
continue;
}
@@ -171,7 +171,7 @@ namespace Content.Server.GameObjects.Components.Disposal
private void UpdateVisualState()
{
if (!Owner.TryGetComponent(out AppearanceComponent appearance))
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
@@ -187,7 +187,7 @@ namespace Content.Server.GameObjects.Components.Disposal
private void AnchoredChanged()
{
if (!Owner.TryGetComponent(out CollidableComponent collidable))
if (!Owner.TryGetComponent(out CollidableComponent? collidable))
{
return;
}

View File

@@ -3,12 +3,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
@@ -85,12 +86,12 @@ namespace Content.Server.GameObjects.Components.Disposal
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent receiver) ||
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
public bool Anchored =>
!Owner.TryGetComponent(out CollidableComponent collidable) ||
!Owner.TryGetComponent(out CollidableComponent? collidable) ||
collidable.Anchored;
[ViewVariables]
@@ -121,8 +122,14 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (!entity.TryGetComponent(out ICollidableComponent? collidable) ||
!collidable.CanCollide)
{
return false;
}
if (!entity.HasComponent<ItemComponent>() &&
!entity.HasComponent<SpeciesComponent>())
!entity.HasComponent<IBodyManagerComponent>())
{
return false;
}
@@ -152,7 +159,7 @@ namespace Content.Server.GameObjects.Components.Disposal
{
TryQueueEngage();
if (entity.TryGetComponent(out IActorComponent actor))
if (entity.TryGetComponent(out IActorComponent? actor))
{
_userInterface.Close(actor.playerSession);
}
@@ -174,7 +181,7 @@ namespace Content.Server.GameObjects.Components.Disposal
private bool TryDrop(IEntity user, IEntity entity)
{
if (!user.TryGetComponent(out HandsComponent hands))
if (!user.TryGetComponent(out HandsComponent? hands))
{
return false;
}
@@ -266,7 +273,7 @@ namespace Content.Server.GameObjects.Components.Disposal
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent receiver))
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
return;
}
@@ -345,7 +352,7 @@ namespace Content.Server.GameObjects.Components.Disposal
private void UpdateVisualState(bool flush)
{
if (!Owner.TryGetComponent(out AppearanceComponent appearance))
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
@@ -481,7 +488,7 @@ namespace Content.Server.GameObjects.Components.Disposal
var collidable = Owner.EnsureComponent<CollidableComponent>();
collidable.AnchoredChanged += UpdateVisualState;
if (Owner.TryGetComponent(out PowerReceiverComponent receiver))
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
receiver.OnPowerStateChanged += PowerStateChanged;
}
@@ -491,12 +498,12 @@ namespace Content.Server.GameObjects.Components.Disposal
public override void OnRemove()
{
if (Owner.TryGetComponent(out ICollidableComponent collidable))
if (Owner.TryGetComponent(out ICollidableComponent? collidable))
{
collidable.AnchoredChanged -= UpdateVisualState;
}
if (Owner.TryGetComponent(out PowerReceiverComponent receiver))
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
receiver.OnPowerStateChanged -= PowerStateChanged;
}
@@ -523,7 +530,7 @@ namespace Content.Server.GameObjects.Components.Disposal
switch (message)
{
case RelayMovementEntityMessage msg:
if (!msg.Entity.TryGetComponent(out HandsComponent hands) ||
if (!msg.Entity.TryGetComponent(out HandsComponent? hands) ||
hands.Count == 0 ||
_gameTiming.CurTime < _lastExitAttempt + ExitAttemptDelay)
{
@@ -552,7 +559,7 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return false;
}
@@ -568,7 +575,7 @@ namespace Content.Server.GameObjects.Components.Disposal
return true;
}
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryDrop(eventArgs.User, eventArgs.Using);
}

View File

@@ -60,7 +60,7 @@ namespace Content.Server.GameObjects.Components
{
connectedClient = null;
if (!Owner.TryGetComponent(out IActorComponent actorComponent))
if (!Owner.TryGetComponent(out IActorComponent? actorComponent))
{
return false;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.Components.VendingMachines;
@@ -379,7 +380,7 @@ namespace Content.Server.GameObjects.Components.Doors
return _powerReceiver.Powered;
}
public bool InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;
@@ -397,22 +398,27 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
if (!tool.UseTool(eventArgs.User, Owner, ToolQuality.Prying)) return false;
if (IsBolted())
bool AirlockCheck()
{
var notify = IoCManager.Resolve<IServerNotifyManager>();
notify.PopupMessage(Owner, eventArgs.User,
Loc.GetString("The airlock's bolts prevent it from being forced!"));
if (IsBolted())
{
var notify = IoCManager.Resolve<IServerNotifyManager>();
notify.PopupMessage(Owner, eventArgs.User,
Loc.GetString("The airlock's bolts prevent it from being forced!"));
return false;
}
if (IsPowered())
{
var notify = IoCManager.Resolve<IServerNotifyManager>();
notify.PopupMessage(Owner, eventArgs.User, Loc.GetString("The powered motors block your efforts!"));
return false;
}
return true;
}
if (IsPowered())
{
var notify = IoCManager.Resolve<IServerNotifyManager>();
notify.PopupMessage(Owner, eventArgs.User, Loc.GetString("The powered motors block your efforts!"));
return true;
}
if (!await tool.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Prying, AirlockCheck)) return false;
if (State == DoorState.Closed)
Open();

View File

@@ -3,9 +3,10 @@ using System.Linq;
using System.Threading;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Movement;
@@ -58,8 +59,7 @@ namespace Content.Server.GameObjects.Components.Doors
private const float DoorStunTime = 5f;
protected bool Safety = true;
[ViewVariables]
private bool _occludes;
[ViewVariables] private bool _occludes;
public override void ExposeData(ObjectSerializer serializer)
{
@@ -111,18 +111,25 @@ namespace Content.Server.GameObjects.Components.Doors
{
return;
}
if (entity.HasComponent(typeof(SpeciesComponent)))
// Disabled because it makes it suck hard to walk through double doors.
if (entity.HasComponent<IBodyManagerComponent>())
{
if (!entity.TryGetComponent<IMoverComponent>(out var mover)) return;
/*
// TODO: temporary hack to fix the physics system raising collision events akwardly.
// E.g. when moving parallel to a door by going off the side of a wall.
var (walking, sprinting) = mover.VelocityDir;
// Also TODO: walking and sprint dir are added together here
// instead of calculating their contribution correctly.
var dotProduct = Vector2.Dot((sprinting + walking).Normalized, (entity.Transform.WorldPosition - Owner.Transform.WorldPosition).Normalized);
if (dotProduct <= -0.9f)
if (dotProduct <= -0.85f)
TryOpen(entity);
*/
TryOpen(entity);
}
}
@@ -144,6 +151,7 @@ namespace Content.Server.GameObjects.Components.Doors
{
return true;
}
return accessReader.IsAllowed(user);
}
@@ -204,6 +212,7 @@ namespace Content.Server.GameObjects.Components.Doors
{
return true;
}
return accessReader.IsAllowed(user);
}
@@ -214,6 +223,7 @@ namespace Content.Server.GameObjects.Components.Doors
Deny();
return;
}
Close();
}
@@ -228,7 +238,7 @@ namespace Content.Server.GameObjects.Components.Doors
foreach (var e in collidesWith)
{
if (!e.TryGetComponent(out StunnableComponent stun)
|| !e.TryGetComponent(out DamageableComponent damage)
|| !e.TryGetComponent(out IDamageableComponent damage)
|| !e.TryGetComponent(out ICollidableComponent otherBody)
|| !Owner.TryGetComponent(out ICollidableComponent body))
continue;
@@ -238,10 +248,11 @@ namespace Content.Server.GameObjects.Components.Doors
if (percentage < 0.1f)
continue;
damage.TakeDamage(DamageType.Brute, DoorCrushDamage);
damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
stun.Paralyze(DoorStunTime);
hitSomeone = true;
}
// If we hit someone, open up after stun (opens right when stun ends)
if (hitSomeone)
{

View File

@@ -1,5 +1,6 @@
using Content.Server.Explosions;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;

View File

@@ -1,6 +1,7 @@
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Weapon;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.Interfaces;
@@ -70,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Fluids
return true;
}
public bool InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent(out MopComponent mopComponent))
{

View File

@@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Fluids
foreach (var spillEntity in entityManager.GetEntitiesAt(spillTileMapGrid.ParentMapId, spillGridCoords.Position))
{
if (!spillEntity.TryGetComponent(out PuddleComponent puddleComponent))
if (!spillEntity.TryGetComponent(out PuddleComponent? puddleComponent))
{
continue;
}

View File

@@ -8,17 +8,19 @@ using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Server.Interfaces.GameObjects.Components.Interaction;
using Content.Shared.GameObjects.Components.Body;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Health.BodySystem;
using Content.Shared.Physics;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Physics.Pull;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystemMessages;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
@@ -140,11 +142,11 @@ namespace Content.Server.GameObjects.Components.GUI
}
}
public bool PutInHand(ItemComponent item)
public bool PutInHand(ItemComponent item, bool mobCheck = true)
{
foreach (var hand in ActivePriorityEnumerable())
{
if (PutInHand(item, hand, false))
if (PutInHand(item, hand, false, mobCheck))
{
OnItemChanged?.Invoke();
@@ -155,10 +157,10 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
public bool PutInHand(ItemComponent item, string index, bool fallback = true)
public bool PutInHand(ItemComponent item, string index, bool fallback = true, bool mobChecks = true)
{
var hand = GetHand(index);
if (!CanPutInHand(item, index) || hand == null)
if (!CanPutInHand(item, index, mobChecks) || hand == null)
{
return fallback && PutInHand(item);
}
@@ -176,19 +178,23 @@ namespace Content.Server.GameObjects.Components.GUI
return success;
}
public void PutInHandOrDrop(ItemComponent item)
public void PutInHandOrDrop(ItemComponent item, bool mobCheck = true)
{
if (!PutInHand(item))
if (!PutInHand(item, mobCheck))
{
item.Owner.Transform.GridPosition = Owner.Transform.GridPosition;
}
}
public bool CanPutInHand(ItemComponent item)
public bool CanPutInHand(ItemComponent item, bool mobCheck = true)
{
if (mobCheck && !ActionBlockerSystem.CanPickup(Owner))
return false;
foreach (var handName in ActivePriorityEnumerable())
{
if (CanPutInHand(item, handName))
// We already did a mobCheck, so let's not waste cycles.
if (CanPutInHand(item, handName, false))
{
return true;
}
@@ -197,8 +203,11 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
public bool CanPutInHand(ItemComponent item, string index)
public bool CanPutInHand(ItemComponent item, string index, bool mobCheck = true)
{
if (mobCheck && !ActionBlockerSystem.CanPickup(Owner))
return false;
return GetHand(index)?.Container.CanInsert(item.Owner) == true;
}
@@ -284,17 +293,17 @@ namespace Content.Server.GameObjects.Components.GUI
return Drop(slot, coords, doMobChecks);
}
public bool Drop(string slot, bool doMobChecks = true)
public bool Drop(string slot, bool mobChecks = true)
{
var hand = GetHand(slot);
if (!CanDrop(slot) || hand?.Entity == null)
if (!CanDrop(slot, mobChecks) || hand?.Entity == null)
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!DroppedInteraction(item, doMobChecks))
if (!DroppedInteraction(item, mobChecks))
return false;
if (!hand.Container.Remove(hand.Entity))
@@ -321,7 +330,7 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Drop(IEntity entity, bool doMobChecks = true)
public bool Drop(IEntity entity, bool mobChecks = true)
{
if (entity == null)
{
@@ -333,7 +342,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, doMobChecks);
return Drop(slot, mobChecks);
}
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true)
@@ -409,13 +418,15 @@ namespace Content.Server.GameObjects.Components.GUI
/// <returns>
/// True if there is an item in the slot and it can be dropped, false otherwise.
/// </returns>
public bool CanDrop(string name)
public bool CanDrop(string name, bool mobCheck = true)
{
var hand = GetHand(name);
if (hand?.Entity == null)
{
if (mobCheck && !ActionBlockerSystem.CanDrop(Owner))
return false;
if (hand?.Entity == null)
return false;
}
return hand.Container.CanRemove(hand.Entity);
}
@@ -537,15 +548,9 @@ namespace Content.Server.GameObjects.Components.GUI
return;
}
var isOwnerContained = ContainerHelpers.TryGetContainer(Owner, out var ownerContainer);
var isPullableContained = ContainerHelpers.TryGetContainer(pullable.Owner, out var pullableContainer);
if (isOwnerContained || isPullableContained)
if (!Owner.IsInSameOrNoContainer(pullable.Owner))
{
if (ownerContainer != pullableContainer)
{
return;
}
return;
}
if (IsPulling)
@@ -554,10 +559,8 @@ namespace Content.Server.GameObjects.Components.GUI
}
PulledObject = pullable.Owner.GetComponent<ICollidableComponent>();
var controller = PulledObject!.EnsureController<PullController>();
controller!.StartPull(Owner.GetComponent<ICollidableComponent>());
AddPullingStatuses();
var controller = PulledObject.EnsureController<PullController>();
controller.StartPull(Owner.GetComponent<ICollidableComponent>());
}
public void MovePulledObject(GridCoordinates puller, GridCoordinates to)
@@ -569,6 +572,46 @@ namespace Content.Server.GameObjects.Components.GUI
}
}
private void MoveEvent(MoveEvent moveEvent)
{
if (moveEvent.Sender != Owner)
{
return;
}
if (!IsPulling)
{
return;
}
PulledObject!.WakeBody();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
if (!(message is PullMessage pullMessage) ||
pullMessage.Puller.Owner != Owner)
{
return;
}
switch (message)
{
case PullStartedMessage msg:
Owner.EntityManager.EventBus.SubscribeEvent<MoveEvent>(EventSource.Local, this, MoveEvent);
AddPullingStatuses(msg.Pulled.Owner);
break;
case PullStoppedMessage msg:
Owner.EntityManager.EventBus.UnsubscribeEvent<MoveEvent>(EventSource.Local, this);
RemovePullingStatuses(msg.Pulled.Owner);
break;
}
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
{
base.HandleNetworkMessage(message, channel, session);
@@ -668,7 +711,7 @@ namespace Content.Server.GameObjects.Components.GUI
Dirty();
if (!message.Entity.TryGetComponent(out ICollidableComponent collidable))
if (!message.Entity.TryGetComponent(out ICollidableComponent? collidable))
{
return;
}
@@ -679,42 +722,34 @@ namespace Content.Server.GameObjects.Components.GUI
}
}
private void AddPullingStatuses()
private void AddPullingStatuses(IEntity pulled)
{
if (PulledObject?.Owner != null &&
PulledObject.Owner.TryGetComponent(out ServerStatusEffectsComponent pulledStatus))
if (pulled.TryGetComponent(out ServerStatusEffectsComponent? pulledStatus))
{
pulledStatus.ChangeStatusEffectIcon(StatusEffect.Pulled,
"/Textures/Interface/StatusEffects/Pull/pulled.png");
}
if (Owner.TryGetComponent(out ServerStatusEffectsComponent ownerStatus))
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? ownerStatus))
{
ownerStatus.ChangeStatusEffectIcon(StatusEffect.Pulling,
"/Textures/Interface/StatusEffects/Pull/pulling.png");
}
}
private void RemovePullingStatuses()
private void RemovePullingStatuses(IEntity pulled)
{
if (PulledObject?.Owner != null &&
PulledObject.Owner.TryGetComponent(out ServerStatusEffectsComponent pulledStatus))
if (pulled.TryGetComponent(out ServerStatusEffectsComponent? pulledStatus))
{
pulledStatus.RemoveStatusEffect(StatusEffect.Pulled);
}
if (Owner.TryGetComponent(out ServerStatusEffectsComponent ownerStatus))
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? ownerStatus))
{
ownerStatus.RemoveStatusEffect(StatusEffect.Pulling);
}
}
public override void StopPull()
{
RemovePullingStatuses();
base.StopPull();
}
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs eventArgs)
{
if (eventArgs.Part.PartType != BodyPartType.Hand)

View File

@@ -173,9 +173,10 @@ namespace Content.Server.GameObjects.Components.GUI
/// </remarks>
/// <param name="slot">The slot to put the item in.</param>
/// <param name="item">The item to insert into the slot.</param>
/// <param name="mobCheck">Whether to perform an ActionBlocker check to the entity.</param>
/// <param name="reason">The translated reason why the item cannot be equipped, if this function returns false. Can be null.</param>
/// <returns>True if the item was successfully inserted, false otherwise.</returns>
public bool Equip(Slots slot, ItemComponent item, out string reason)
public bool Equip(Slots slot, ItemComponent item, bool mobCheck, out string reason)
{
if (item == null)
{
@@ -183,7 +184,7 @@ namespace Content.Server.GameObjects.Components.GUI
"Clothing must be passed here. To remove some clothing from a slot, use Unequip()");
}
if (!CanEquip(slot, item, out reason))
if (!CanEquip(slot, item, mobCheck, out reason))
{
return false;
}
@@ -203,9 +204,9 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Equip(Slots slot, ItemComponent item) => Equip(slot, item, out var _);
public bool Equip(Slots slot, ItemComponent item, bool mobCheck = true) => Equip(slot, item, mobCheck, out var _);
public bool Equip(Slots slot, IEntity entity) => Equip(slot, entity.GetComponent<ItemComponent>());
public bool Equip(Slots slot, IEntity entity, bool mobCheck = true) => Equip(slot, entity.GetComponent<ItemComponent>(), mobCheck);
/// <summary>
/// Checks whether an item can be put in the specified slot.
@@ -214,12 +215,12 @@ namespace Content.Server.GameObjects.Components.GUI
/// <param name="item">The item to check for.</param>
/// <param name="reason">The translated reason why the item cannot be equiped, if this function returns false. Can be null.</param>
/// <returns>True if the item can be inserted into the specified slot.</returns>
public bool CanEquip(Slots slot, ItemComponent item, out string reason)
public bool CanEquip(Slots slot, ItemComponent item, bool mobCheck, out string reason)
{
var pass = false;
reason = null;
if (!ActionBlockerSystem.CanEquip(Owner))
if (mobCheck && !ActionBlockerSystem.CanEquip(Owner))
return false;
if (item is ClothingComponent clothing)
@@ -248,18 +249,19 @@ namespace Content.Server.GameObjects.Components.GUI
return pass && _slotContainers[slot].CanInsert(item.Owner);
}
public bool CanEquip(Slots slot, ItemComponent item) => CanEquip(slot, item, out var _);
public bool CanEquip(Slots slot, ItemComponent item, bool mobCheck = true) => CanEquip(slot, item, mobCheck, out var _);
public bool CanEquip(Slots slot, IEntity entity) => CanEquip(slot, entity.GetComponent<ItemComponent>());
public bool CanEquip(Slots slot, IEntity entity, bool mobCheck = true) => CanEquip(slot, entity.GetComponent<ItemComponent>(), mobCheck);
/// <summary>
/// Drops the item in a slot.
/// </summary>
/// <param name="slot">The slot to drop the item from.</param>
/// <returns>True if an item was dropped, false otherwise.</returns>
public bool Unequip(Slots slot)
/// <param name="mobCheck">Whether to perform an ActionBlocker check to the entity.</param>
public bool Unequip(Slots slot, bool mobCheck = true)
{
if (!CanUnequip(slot))
if (!CanUnequip(slot, mobCheck))
{
return false;
}
@@ -288,16 +290,17 @@ namespace Content.Server.GameObjects.Components.GUI
/// Checks whether an item can be dropped from the specified slot.
/// </summary>
/// <param name="slot">The slot to check for.</param>
/// <param name="mobCheck">Whether to perform an ActionBlocker check to the entity.</param>
/// <returns>
/// True if there is an item in the slot and it can be dropped, false otherwise.
/// </returns>
public bool CanUnequip(Slots slot)
public bool CanUnequip(Slots slot, bool mobCheck = true)
{
if (!ActionBlockerSystem.CanUnequip(Owner))
if (mobCheck && !ActionBlockerSystem.CanUnequip(Owner))
return false;
var InventorySlot = _slotContainers[slot];
return InventorySlot.ContainedEntity != null && InventorySlot.CanRemove(InventorySlot.ContainedEntity);
var inventorySlot = _slotContainers[slot];
return inventorySlot.ContainedEntity != null && inventorySlot.CanRemove(inventorySlot.ContainedEntity);
}
/// <summary>
@@ -398,7 +401,7 @@ namespace Content.Server.GameObjects.Components.GUI
if (activeHand != null && activeHand.Owner.TryGetComponent(out ItemComponent clothing))
{
hands.Drop(hands.ActiveHand);
if (!Equip(msg.Inventoryslot, clothing, out var reason))
if (!Equip(msg.Inventoryslot, clothing, true, out var reason))
{
hands.PutInHand(clothing);
@@ -434,7 +437,7 @@ namespace Content.Server.GameObjects.Components.GUI
var activeHand = hands.GetActiveHand;
if (activeHand != null && GetSlotItem(msg.Inventoryslot) == null)
{
var canEquip = CanEquip(msg.Inventoryslot, activeHand, out var reason);
var canEquip = CanEquip(msg.Inventoryslot, activeHand, true, out var reason);
_hoverEntity = new KeyValuePair<Slots, (EntityUid entity, bool fits)>(msg.Inventoryslot, (activeHand.Owner.Uid, canEquip));
Dirty();

View File

@@ -137,7 +137,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!inventory.CanEquip(slot, item))
if (!inventory.CanEquip(slot, item, false))
{
_notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot equip that there!", Owner));
return false;
@@ -162,7 +162,7 @@ namespace Content.Server.GameObjects.Components.GUI
if (result != DoAfterStatus.Finished) return;
userHands.Drop(item!.Owner, false);
inventory.Equip(slot, item!.Owner);
inventory.Equip(slot, item!.Owner, false);
UpdateSubscribed();
}
@@ -202,7 +202,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!hands.CanPutInHand(item, hand))
if (!hands.CanPutInHand(item, hand, false))
{
_notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot put that there!", Owner));
return false;
@@ -227,7 +227,7 @@ namespace Content.Server.GameObjects.Components.GUI
if (result != DoAfterStatus.Finished) return;
userHands.Drop(hand, false);
hands.PutInHand(item, hand, false);
hands.PutInHand(item!, hand, false, false);
UpdateSubscribed();
}
@@ -253,7 +253,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!inventory.CanUnequip(slot))
if (!inventory.CanUnequip(slot, false))
{
_notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot unequip that!", Owner));
return false;
@@ -277,7 +277,7 @@ namespace Content.Server.GameObjects.Components.GUI
if (result != DoAfterStatus.Finished) return;
var item = inventory.GetSlotItem(slot);
inventory.Unequip(slot);
inventory.Unequip(slot, false);
userHands.PutInHandOrDrop(item);
UpdateSubscribed();
}
@@ -304,7 +304,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!hands.CanDrop(hand))
if (!hands.CanDrop(hand, false))
{
_notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot drop that!", Owner));
return false;
@@ -329,7 +329,7 @@ namespace Content.Server.GameObjects.Components.GUI
var item = hands.GetItem(hand);
hands.Drop(hand, false);
userHands.PutInHandOrDrop(item);
userHands.PutInHandOrDrop(item!);
UpdateSubscribed();
}
@@ -364,8 +364,6 @@ namespace Content.Server.GameObjects.Components.GUI
else
TakeItemFromHands(user, handMessage.Hand);
break;
default:
break;
}
}
}

View File

@@ -1,10 +1,12 @@
using Content.Server.GameObjects.Components.Damage;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
@@ -18,7 +20,7 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Gravity
{
[RegisterComponent]
public class GravityGeneratorComponent: SharedGravityGeneratorComponent, IInteractUsing, IBreakAct, IInteractHand
public class GravityGeneratorComponent : SharedGravityGeneratorComponent, IInteractUsing, IBreakAct, IInteractHand
{
private BoundUserInterface _userInterface;
@@ -97,19 +99,17 @@ namespace Content.Server.GameObjects.Components.Gravity
return true;
}
public bool InteractUsing(InteractUsingEventArgs eventArgs)
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent(out WelderComponent tool))
return false;
if (!tool.UseTool(eventArgs.User, Owner, ToolQuality.Welding, 5f))
if (!await tool.UseTool(eventArgs.User, Owner, 2f, ToolQuality.Welding, 5f))
return false;
// Repair generator
var damageable = Owner.GetComponent<DamageableComponent>();
var breakable = Owner.GetComponent<BreakableComponent>();
damageable.HealAllDamage();
breakable.broken = false;
breakable.FixAllDamage();
_intact = true;
var notifyManager = IoCManager.Resolve<IServerNotifyManager>();
@@ -130,13 +130,16 @@ namespace Content.Server.GameObjects.Components.Gravity
if (!Intact)
{
MakeBroken();
} else if (!Powered)
}
else if (!Powered)
{
MakeUnpowered();
} else if (!SwitchedOn)
}
else if (!SwitchedOn)
{
MakeOff();
} else
}
else
{
MakeOn();
}

View File

@@ -1,71 +0,0 @@
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Healing
{
[RegisterComponent]
public class HealingComponent : Component, IAfterInteract, IUse
{
public override string Name => "Healing";
public int Heal = 100;
public DamageType Damage = DamageType.Brute;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref Heal, "heal", 100);
serializer.DataField(ref Damage, "damage", DamageType.Brute);
}
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!InteractionChecks.InRangeUnobstructed(eventArgs)) return;
if (eventArgs.Target == null)
{
return;
}
if (!eventArgs.Target.TryGetComponent(out DamageableComponent damagecomponent)) return;
if (Owner.TryGetComponent(out StackComponent stackComponent))
{
if (!stackComponent.Use(1))
{
Owner.Delete();
return;
}
damagecomponent.TakeHealing(Damage, Heal);
return;
}
damagecomponent.TakeHealing(Damage, Heal);
Owner.Delete();
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out DamageableComponent damagecomponent)) return false;
if (Owner.TryGetComponent(out StackComponent stackComponent))
{
if (!stackComponent.Use(1))
{
Owner.Delete();
return false;
}
damagecomponent.TakeHealing(Damage, Heal);
return false;
}
damagecomponent.TakeHealing(Damage, Heal);
Owner.Delete();
return false;
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.GameObjects.Components.GUI;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Clothing;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power;
@@ -57,7 +58,7 @@ namespace Content.Server.GameObjects.Components.Interactable
[ViewVariables]
public bool Activated { get; private set; }
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.HasComponent<BatteryComponent>()) return false;
@@ -276,7 +277,7 @@ namespace Content.Server.GameObjects.Components.Interactable
return;
}
var cell = Owner.EntityManager.SpawnEntity("PowerCellSmallHyper", Owner.Transform.GridPosition);
var cell = Owner.EntityManager.SpawnEntity("PowerCellSmallStandard", Owner.Transform.GridPosition);
_cellContainer.Insert(cell);
}
}

View File

@@ -34,7 +34,7 @@ namespace Content.Server.GameObjects.Components.Interactable
serializer.DataField(ref _toolComponentNeeded, "toolComponentNeeded", true);
}
public void TryPryTile(IEntity user, GridCoordinates clickLocation)
public async void TryPryTile(IEntity user, GridCoordinates clickLocation)
{
if (!Owner.TryGetComponent<ToolComponent>(out var tool) && _toolComponentNeeded)
return;
@@ -51,7 +51,7 @@ namespace Content.Server.GameObjects.Components.Interactable
if (!tileDef.CanCrowbar) return;
if (_toolComponentNeeded && !tool.UseTool(user, null, ToolQuality.Prying))
if (_toolComponentNeeded && !await tool!.UseTool(user, null, 0f, ToolQuality.Prying))
return;
var underplating = _tileDefinitionManager["underplating"];

View File

@@ -106,7 +106,7 @@ namespace Content.Server.GameObjects.Components.Interactable
foreach (var entity in entities)
{
if (entity.TryGetComponent(out AnchorableComponent anchorable))
if (entity.TryGetComponent(out AnchorableComponent? anchorable))
{
anchorable.TryAnchor(player.AttachedEntity, force: true);
}
@@ -151,7 +151,7 @@ namespace Content.Server.GameObjects.Components.Interactable
foreach (var entity in entities)
{
if (entity.TryGetComponent(out AnchorableComponent anchorable))
if (entity.TryGetComponent(out AnchorableComponent? anchorable))
{
anchorable.TryUnAnchor(player.AttachedEntity, force: true);
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
@@ -89,11 +91,31 @@ namespace Content.Server.GameObjects.Components.Interactable
serializer.DataField(this, collection => UseSoundCollection, "useSoundCollection", string.Empty);
}
public virtual bool UseTool(IEntity user, IEntity target, ToolQuality toolQualityNeeded)
public virtual async Task<bool> UseTool(IEntity user, IEntity target, float doAfterDelay, ToolQuality toolQualityNeeded, Func<bool> doAfterCheck = null)
{
if (!HasQuality(toolQualityNeeded) || !ActionBlockerSystem.CanInteract(user))
return false;
if (doAfterDelay > 0f)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(user, doAfterDelay / SpeedModifier, default, target)
{
ExtraCheck = doAfterCheck,
BreakOnDamage = false, // TODO: Change this to true once breathing is fixed.
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
}
PlayUseSound();
return true;

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Items.Storage;
@@ -97,16 +98,37 @@ namespace Content.Server.GameObjects.Components.Interactable
return new WelderComponentState(FuelCapacity, Fuel, WelderLit);
}
public override bool UseTool(IEntity user, IEntity target, ToolQuality toolQualityNeeded)
public override async Task<bool> UseTool(IEntity user, IEntity target, float doAfterDelay, ToolQuality toolQualityNeeded, Func<bool>? doAfterCheck = null)
{
var canUse = base.UseTool(user, target, toolQualityNeeded);
bool ExtraCheck()
{
var extraCheck = doAfterCheck?.Invoke() ?? true;
if (!CanWeld(DefaultFuelCost))
{
_notifyManager.PopupMessage(target, user, "Can't weld!");
return false;
}
return extraCheck;
}
var canUse = await base.UseTool(user, target, doAfterDelay, toolQualityNeeded, ExtraCheck);
return toolQualityNeeded.HasFlag(ToolQuality.Welding) ? canUse && TryWeld(DefaultFuelCost, user) : canUse;
}
public bool UseTool(IEntity user, IEntity target, ToolQuality toolQualityNeeded, float fuelConsumed)
public async Task<bool> UseTool(IEntity user, IEntity target, float doAfterDelay, ToolQuality toolQualityNeeded, float fuelConsumed, Func<bool>? doAfterCheck = null)
{
return base.UseTool(user, target, toolQualityNeeded) && TryWeld(fuelConsumed, user);
bool ExtraCheck()
{
var extraCheck = doAfterCheck?.Invoke() ?? true;
return extraCheck && CanWeld(fuelConsumed);
}
return await base.UseTool(user, target, doAfterDelay, toolQualityNeeded, ExtraCheck) && TryWeld(fuelConsumed, user);
}
private bool TryWeld(float value, IEntity? user = null, bool silent = false)
@@ -236,11 +258,12 @@ namespace Content.Server.GameObjects.Components.Interactable
if (TryWeld(5, victim, silent: true))
{
PlaySoundCollection(WeldSoundCollection);
chat.EntityMe(victim, Loc.GetString("welds {0:their} every orifice closed! It looks like {0:theyre} trying to commit suicide!", victim)); //TODO: theyre macro
chat.EntityMe(victim, Loc.GetString("welds {0:their} every orifice closed! It looks like {0:theyre} trying to commit suicide!", victim));
return SuicideKind.Heat;
}
chat.EntityMe(victim, Loc.GetString("bashes {0:themselves} with the {1}!", victim, Owner.Name));
return SuicideKind.Brute;
return SuicideKind.Blunt;
}
public void SolutionChanged(SolutionChangeEventArgs eventArgs)

View File

@@ -112,7 +112,7 @@ namespace Content.Server.GameObjects.Components.Items.Clothing
private bool TryEquip(InventoryComponent inv, Slots slot, IEntity user)
{
if (!inv.Equip(slot, this, out var reason))
if (!inv.Equip(slot, this, true, out var reason))
{
if (reason != null)
_serverNotifyManager.PopupMessage(Owner, user, reason);

View File

@@ -1,10 +1,13 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Storage;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
@@ -168,7 +171,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
continue;
// only items that can be stored in an inventory, or a mob, can be eaten by a locker
if (!entity.HasComponent<StorableComponent>() && !entity.HasComponent<SpeciesComponent>())
if (!entity.HasComponent<StorableComponent>() &&
!entity.HasComponent<BodyManagerComponent>())
continue;
if (!AddToContents(entity))
@@ -356,7 +360,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return Contents.CanInsert(entity);
}
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (Open)
@@ -374,10 +378,9 @@ namespace Content.Server.GameObjects.Components.Items.Storage
if (!eventArgs.Using.TryGetComponent(out WelderComponent tool))
return false;
if (!tool.UseTool(eventArgs.User, Owner, ToolQuality.Welding, 1f))
if (!await tool.UseTool(eventArgs.User, Owner, 1f, ToolQuality.Welding, 1f))
return false;
IsWeldedShut ^= true;
return true;
}

Some files were not shown because too many files have changed in this diff Show More