Merge remote-tracking branch 'upstream/master' into ed-05-08-2024-upstream

# Conflicts:
#	Content.Shared/Inventory/InventorySystem.Equip.cs
#	Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
This commit is contained in:
Ed
2024-08-12 14:40:15 +03:00
830 changed files with 27583 additions and 19536 deletions

View File

@@ -1,15 +1,7 @@
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
namespace Content.Server.Access.Components;
namespace Content.Server.Access.Components
{
[RegisterComponent]
public sealed partial class AgentIDCardComponent : Component
{
/// <summary>
/// Set of job icons that the agent ID card can show.
/// </summary>
[DataField]
public HashSet<ProtoId<StatusIconPrototype>> Icons;
}
}
/// <summary>
/// Allows an ID card to copy accesses from other IDs and to change the name, job title and job icon via an interface.
/// </summary>
[RegisterComponent]
public sealed partial class AgentIDCardComponent : Component { }

View File

@@ -67,7 +67,7 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon ?? "", component.Icons);
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon);
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
}
@@ -101,7 +101,7 @@ namespace Content.Server.Access.Systems
_cardSystem.TryChangeJobDepartment(uid, job, idCard);
}
private bool TryFindJobProtoFromIcon(StatusIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
{
foreach (var jobPrototype in _prototypeManager.EnumeratePrototypes<JobPrototype>())
{

View File

@@ -105,6 +105,31 @@ public sealed class ActionOnInteractSystem : EntitySystem
}
}
// Then EntityWorld target actions
var entWorldOptions = GetValidActions<EntityWorldTargetActionComponent>(actionEnts, args.CanReach);
for (var i = entWorldOptions.Count - 1; i >= 0; i--)
{
var action = entWorldOptions[i];
if (!_actions.ValidateEntityWorldTarget(args.User, args.Target, args.ClickLocation, action))
entWorldOptions.RemoveAt(i);
}
if (entWorldOptions.Count > 0)
{
var (entActId, entAct) = _random.Pick(entWorldOptions);
if (entAct.Event != null)
{
entAct.Event.Performer = args.User;
entAct.Event.Action = entActId;
entAct.Event.Entity = args.Target;
entAct.Event.Coords = args.ClickLocation;
}
_actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
args.Handled = true;
return;
}
// else: try world target actions
var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach);
for (var i = options.Count - 1; i >= 0; i--)

View File

@@ -17,7 +17,7 @@ public sealed class BanPanelCommand : LocalizedCommands
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("cmd-banpanel-server"));
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -17,7 +17,7 @@ namespace Content.Server.Administration.Commands
{
if (shell.Player is not { } player)
{
shell.WriteLine("shell-server-cannot");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -17,10 +17,9 @@ namespace Content.Server.Administration.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
if (shell.Player is not { } player)
{
shell.WriteLine("shell-only-players-can-run-this-command");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -132,6 +132,6 @@ public sealed class ExplosionCommand : IConsoleCommand
}
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity);
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
}
}

View File

@@ -15,10 +15,9 @@ public sealed class FaxUiCommand : IConsoleCommand
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
if (shell.Player is not { } player)
{
shell.WriteLine("shell-only-players-can-run-this-command");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -16,10 +16,9 @@ public sealed class FollowCommand : IConsoleCommand
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -16,7 +16,7 @@ public sealed class OpenAdminLogsCommand : IConsoleCommand
{
if (shell.Player is not { } player)
{
shell.WriteLine("This does not work from the server console.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -17,7 +17,7 @@ public sealed class OpenAdminNotesCommand : IConsoleCommand
{
if (shell.Player is not { } player)
{
shell.WriteError("This does not work from the server console.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -28,7 +28,7 @@ public sealed class OpenUserVisibleNotesCommand : IConsoleCommand
if (shell.Player is not { } player)
{
shell.WriteError("This does not work from the server console.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -0,0 +1,56 @@
using System.Linq;
using Content.Server.EUI;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class PlayerPanelCommand : LocalizedCommands
{
[Dependency] private readonly IPlayerLocator _locator = default!;
[Dependency] private readonly EuiManager _euis = default!;
[Dependency] private readonly IPlayerManager _players = default!;
public override string Command => "playerpanel";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } admin)
{
shell.WriteError(Loc.GetString("cmd-playerpanel-server"));
return;
}
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-arguments"));
return;
}
var queriedPlayer = await _locator.LookupIdByNameOrIdAsync(args[0]);
if (queriedPlayer == null)
{
shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-player"));
return;
}
var ui = new PlayerPanelEui(queriedPlayer);
_euis.OpenEui(ui, admin);
ui.SetPlayerState();
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
return CompletionResult.FromHintOptions(options, LocalizationManager.GetString("cmd-playerpanel-completion"));
}
return CompletionResult.Empty;
}
}

View File

@@ -16,7 +16,7 @@ namespace Content.Server.Administration.Commands
{
if (shell.Player == null)
{
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -0,0 +1,210 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Notes;
using Content.Server.Administration.Systems;
using Content.Server.Database;
using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Server.Player;
using Robust.Shared.Player;
namespace Content.Server.Administration;
public sealed class PlayerPanelEui : BaseEui
{
[Dependency] private readonly IAdminManager _admins = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IAdminNotesManager _notesMan = default!;
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
private readonly LocatedPlayerData _targetPlayer;
private int? _notes;
private int? _bans;
private int? _roleBans;
private int _sharedConnections;
private bool? _whitelisted;
private TimeSpan _playtime;
private bool _frozen;
private bool _canFreeze;
private bool _canAhelp;
public PlayerPanelEui(LocatedPlayerData player)
{
IoCManager.InjectDependencies(this);
_targetPlayer = player;
}
public override void Opened()
{
base.Opened();
_admins.OnPermsChanged += OnPermsChanged;
}
public override void Closed()
{
base.Closed();
_admins.OnPermsChanged -= OnPermsChanged;
}
public override EuiStateBase GetNewState()
{
return new PlayerPanelEuiState(_targetPlayer.UserId,
_targetPlayer.Username,
_playtime,
_notes,
_bans,
_roleBans,
_sharedConnections,
_whitelisted,
_canFreeze,
_frozen,
_canAhelp);
}
private void OnPermsChanged(AdminPermsChangedEventArgs args)
{
if (args.Player != Player)
return;
SetPlayerState();
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
ICommonSession? session;
switch (msg)
{
case PlayerPanelFreezeMessage freezeMsg:
if (!_admins.IsAdmin(Player) ||
!_entity.TrySystem<AdminFrozenSystem>(out var frozenSystem) ||
!_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
session.AttachedEntity == null)
return;
if (_entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity))
{
_adminLog.Add(LogType.Action,$"{Player:actor} unfroze {_entity.ToPrettyString(session.AttachedEntity):subject}");
_entity.RemoveComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
SetPlayerState();
return;
}
if (freezeMsg.Mute)
{
_adminLog.Add(LogType.Action,$"{Player:actor} froze and muted {_entity.ToPrettyString(session.AttachedEntity):subject}");
frozenSystem.FreezeAndMute(session.AttachedEntity.Value);
}
else
{
_adminLog.Add(LogType.Action,$"{Player:actor} froze {_entity.ToPrettyString(session.AttachedEntity):subject}");
_entity.EnsureComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
}
SetPlayerState();
break;
case PlayerPanelLogsMessage:
if (!_admins.HasAdminFlag(Player, AdminFlags.Logs))
return;
_adminLog.Add(LogType.Action, $"{Player:actor} opened logs on {_targetPlayer.Username:subject}");
var ui = new AdminLogsEui();
_eui.OpenEui(ui, Player);
ui.SetLogFilter(search: _targetPlayer.Username);
break;
case PlayerPanelDeleteMessage:
case PlayerPanelRejuvenationMessage:
if (!_admins.HasAdminFlag(Player, AdminFlags.Debug) ||
!_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
session.AttachedEntity == null)
return;
if (msg is PlayerPanelRejuvenationMessage)
{
_adminLog.Add(LogType.Action,$"{Player:actor} rejuvenated {_entity.ToPrettyString(session.AttachedEntity):subject}");
if (!_entity.TrySystem<RejuvenateSystem>(out var rejuvenate))
return;
rejuvenate.PerformRejuvenate(session.AttachedEntity.Value);
}
else
{
_adminLog.Add(LogType.Action,$"{Player:actor} deleted {_entity.ToPrettyString(session.AttachedEntity):subject}");
_entity.DeleteEntity(session.AttachedEntity);
}
break;
}
}
public async void SetPlayerState()
{
if (!_admins.IsAdmin(Player))
{
Close();
return;
}
_playtime = (await _db.GetPlayTimes(_targetPlayer.UserId))
.Where(p => p.Tracker == "Overall")
.Select(p => p.TimeSpent)
.FirstOrDefault();
if (_notesMan.CanView(Player))
{
_notes = (await _notesMan.GetAllAdminRemarks(_targetPlayer.UserId)).Count;
}
else
{
_notes = null;
}
_sharedConnections = _player.Sessions.Count(s => s.Channel.RemoteEndPoint.Address.Equals(_targetPlayer.LastAddress) && s.UserId != _targetPlayer.UserId);
// Apparently the Bans flag is also used for whitelists
if (_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
// The only way to distinguish whether a role ban is the same is to compare the ban time.
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
}
else
{
_whitelisted = null;
_bans = null;
_roleBans = null;
}
if (_player.TryGetSessionById(_targetPlayer.UserId, out var session))
{
_canFreeze = session.AttachedEntity != null;
_frozen = _entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity);
}
else
{
_canFreeze = false;
}
if (_admins.HasAdminFlag(Player, AdminFlags.Adminhelp))
{
_canAhelp = true;
}
else
{
_canAhelp = false;
}
StateDirty();
}
}

View File

@@ -105,7 +105,7 @@ public sealed partial class AdminVerbSystem
var coords = _transformSystem.GetMapCoordinates(args.Target);
Timer.Spawn(_gameTiming.TickPeriod,
() => _explosionSystem.QueueExplosion(coords, ExplosionSystem.DefaultExplosionPrototypeId,
4, 1, 2, maxTileBreak: 0), // it gibs, damage doesn't need to be high.
4, 1, 2, args.Target, maxTileBreak: 0), // it gibs, damage doesn't need to be high.
CancellationToken.None);
_bodySystem.GibBody(args.Target);

View File

@@ -724,15 +724,7 @@ public sealed partial class AdminVerbSystem
if (!int.TryParse(amount, out var result))
return;
if (result > 0)
{
ballisticAmmo.UnspawnedCount = result;
}
else
{
ballisticAmmo.UnspawnedCount = 0;
}
_gun.SetBallisticUnspawned((args.Target, ballisticAmmo), result);
_gun.UpdateBallisticAppearance(args.Target, ballisticAmmo);
});
},

View File

@@ -35,6 +35,9 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Content.Server.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Robust.Shared.Physics.Components;
using static Content.Shared.Configurable.ConfigurationComponent;
@@ -68,6 +71,8 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly StationSpawningSystem _spawning = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly AdminFrozenSystem _freeze = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SiliconLawSystem _siliconLawSystem = default!;
private readonly Dictionary<ICommonSession, List<EditSolutionsEui>> _openSolutionUis = new();
@@ -208,6 +213,15 @@ namespace Content.Server.Administration.Systems
ConfirmationPopup = true,
Impact = LogImpact.High,
});
// PlayerPanel
args.Verbs.Add(new Verb
{
Text = Loc.GetString("admin-player-actions-player-panel"),
Category = VerbCategory.Admin,
Act = () => _console.ExecuteCommand(player, $"playerpanel \"{targetActor.PlayerSession.UserId}\""),
Impact = LogImpact.Low
});
}
// Freeze
@@ -329,6 +343,25 @@ namespace Content.Server.Administration.Systems
Impact = LogImpact.Low
});
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("silicon-law-ui-verb"),
Category = VerbCategory.Admin,
Act = () =>
{
var ui = new SiliconLawEui(_siliconLawSystem, EntityManager, _adminManager);
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
{
return;
}
_euiManager.OpenEui(ui, session);
ui.UpdateLaws(lawBoundComponent, args.Target);
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});
}
}
}

View File

@@ -217,7 +217,7 @@ public sealed partial class AnomalySystem
msg.PushNewline();
if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
msg.AddMarkup(Loc.GetString("anomaly-behavior-unknown"));
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
else
{
if (anomalyComp.CurrentBehavior != null)

View File

@@ -8,10 +8,12 @@ using Content.Shared.Alert;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.HealthExaminable;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
@@ -54,6 +56,7 @@ public sealed class BloodstreamSystem : EntitySystem
SubscribeLocalEvent<BloodstreamComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<BloodstreamComponent, GenerateDnaEvent>(OnDnaGenerated);
}
private void OnMapInit(Entity<BloodstreamComponent> ent, ref MapInitEvent args)
@@ -183,8 +186,18 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
{
donorComp.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
RaiseLocalEvent(entity.Owner, ref ev);
}
// Fill blood solution with BLOOD
bloodSolution.AddReagent(entity.Comp.BloodReagent, entity.Comp.BloodMaxVolume - bloodSolution.Volume);
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
private void OnDamageChanged(Entity<BloodstreamComponent> ent, ref DamageChangedEvent args)
@@ -242,20 +255,20 @@ public sealed class BloodstreamSystem : EntitySystem
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding, but less than profusely.
else if (ent.Comp.BleedAmount > 0)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
}
}
@@ -349,7 +362,7 @@ public sealed class BloodstreamSystem : EntitySystem
}
if (amount >= 0)
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, out _);
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, null, GetEntityBloodData(uid));
// Removal is more involved,
// since we also wanna handle moving it to the temporary solution
@@ -370,10 +383,7 @@ public sealed class BloodstreamSystem : EntitySystem
tempSolution.AddSolution(temp, _prototypeManager);
}
if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false))
{
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
}
_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false);
tempSolution.RemoveAllSolution();
}
@@ -436,10 +446,7 @@ public sealed class BloodstreamSystem : EntitySystem
_solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value);
}
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
{
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
}
_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid);
}
/// <summary>
@@ -464,6 +471,40 @@ public sealed class BloodstreamSystem : EntitySystem
component.BloodReagent = reagent;
if (currentVolume > 0)
_solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, out _);
_solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, null, GetEntityBloodData(uid));
}
private void OnDnaGenerated(Entity<BloodstreamComponent> entity, ref GenerateDnaEvent args)
{
if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
{
foreach (var reagent in bloodSolution.Contents)
{
List<ReagentData> reagentData = reagent.Reagent.EnsureReagentData();
reagentData.RemoveAll(x => x is DnaData);
reagentData.AddRange(GetEntityBloodData(entity.Owner));
}
}
}
/// <summary>
/// Get the reagent data for blood that a specific entity should have.
/// </summary>
public List<ReagentData> GetEntityBloodData(EntityUid uid)
{
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();
if (TryComp<DnaComponent>(uid, out var donorComp))
{
dnaData.DNA = donorComp.DNA;
} else
{
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
}
bloodData.Add(dnaData);
return bloodData;
}
}

View File

@@ -48,7 +48,7 @@ public sealed class BotanySwabSystem : EntitySystem
{
Broadcast = true,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
});
}

View File

@@ -38,7 +38,7 @@ public sealed class MutationSystem : EntitySystem
}
// Add up everything in the bits column and put the number here.
const int totalbits = 275;
const int totalbits = 262;
#pragma warning disable IDE0055 // disable formatting warnings because this looks more readable
// Tolerances (55)
@@ -65,10 +65,10 @@ public sealed class MutationSystem : EntitySystem
// Kill the plant (30)
MutateBool(ref seed.Viable , false, 30, totalbits, severity);
// Fun (90)
// Fun (72)
MutateBool(ref seed.Seedless , true , 10, totalbits, severity);
MutateBool(ref seed.Slip , true , 10, totalbits, severity);
MutateBool(ref seed.Sentient , true , 10, totalbits, severity);
MutateBool(ref seed.Sentient , true , 2 , totalbits, severity);
MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
@@ -115,10 +115,10 @@ public sealed class MutationSystem : EntitySystem
CrossFloat(ref result.Production, a.Production);
CrossFloat(ref result.Potency, a.Potency);
// we do not transfer Sentient to another plant to avoid ghost role spam
CrossBool(ref result.Seedless, a.Seedless);
CrossBool(ref result.Viable, a.Viable);
CrossBool(ref result.Slip, a.Slip);
CrossBool(ref result.Sentient, a.Sentient);
CrossBool(ref result.Ligneous, a.Ligneous);
CrossBool(ref result.Bioluminescent, a.Bioluminescent);
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);

View File

@@ -298,8 +298,17 @@ public sealed class PlantHolderSystem : EntitySystem
{
healthOverride = component.Health;
}
component.Seed.Unique = false;
var seed = _botany.SpawnSeedPacket(component.Seed, Transform(args.User).Coordinates, args.User, healthOverride);
var packetSeed = component.Seed;
if (packetSeed.Sentient)
{
packetSeed = packetSeed.Clone(); // clone before modifying the seed
packetSeed.Sentient = false;
}
else
{
packetSeed.Unique = false;
}
var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
_randomHelper.RandomOffset(seed, 0.25f);
var displayName = Loc.GetString(component.Seed.DisplayName);
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
@@ -626,8 +635,15 @@ public sealed class PlantHolderSystem : EntitySystem
}
else if (component.Age < 0) // Revert back to seed packet!
{
var packetSeed = component.Seed;
if (packetSeed.Sentient)
{
if (!packetSeed.Unique) // clone if necessary before modifying the seed
packetSeed = packetSeed.Clone();
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
}
// will put it in the trays hands if it has any, please do not try doing this
_botany.SpawnSeedPacket(component.Seed, Transform(uid).Coordinates, uid);
_botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
RemovePlant(uid, component);
component.ForceUpdate = true;
Update(uid, component);

View File

@@ -42,12 +42,19 @@ public sealed class SeedExtractorSystem : EntitySystem
var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1);
var coords = Transform(uid).Coordinates;
var packetSeed = seed;
if (packetSeed.Sentient)
{
if (!packetSeed.Unique) // clone if necessary before modifying the seed
packetSeed = packetSeed.Clone();
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
}
if (amount > 1)
seed.Unique = false;
packetSeed.Unique = false;
for (var i = 0; i < amount; i++)
{
_botanySystem.SpawnSeedPacket(seed, coords, args.User);
_botanySystem.SpawnSeedPacket(packetSeed, coords, args.User);
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Content.Server.Chat.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -16,7 +16,7 @@ namespace Content.Server.Chat.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -1,6 +1,7 @@
using Content.Server.GameTicking;
using Content.Server.Popups;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Mind;
using Robust.Shared.Console;
using Robust.Shared.Enums;
@@ -22,7 +23,7 @@ namespace Content.Server.Chat.Commands
{
if (shell.Player is not { } player)
{
shell.WriteLine(Loc.GetString("shell-cannot-run-command-from-server"));
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
@@ -32,15 +33,13 @@ namespace Content.Server.Chat.Commands
var minds = _e.System<SharedMindSystem>();
// This check also proves mind not-null for at the end when the mob is ghosted.
if (!minds.TryGetMind(player, out var mindId, out var mind) ||
mind.OwnedEntity is not { Valid: true } victim)
if (!minds.TryGetMind(player, out var mindId, out var mindComp) ||
mindComp.OwnedEntity is not { Valid: true } victim)
{
shell.WriteLine(Loc.GetString("suicide-command-no-mind"));
return;
}
var gameTicker = _e.System<GameTicker>();
var suicideSystem = _e.System<SuicideSystem>();
if (_e.HasComponent<AdminFrozenComponent>(victim))
@@ -53,14 +52,6 @@ namespace Content.Server.Chat.Commands
}
if (suicideSystem.Suicide(victim))
{
// Prevent the player from returning to the body.
// Note that mind cannot be null because otherwise victim would be null.
gameTicker.OnGhostAttempt(mindId, false, mind: mind);
return;
}
if (gameTicker.OnGhostAttempt(mindId, true, mind: mind))
return;
shell.WriteLine(Loc.GetString("ghost-command-denied"));

View File

@@ -16,7 +16,7 @@ namespace Content.Server.Chat.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -257,7 +257,7 @@ namespace Content.Server.Chat.Managers
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
_mommiLink.SendOOCMessage(player.Name, message);
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
}

View File

@@ -1,141 +1,154 @@
using Content.Server.Administration.Logs;
using Content.Server.Popups;
using Content.Server.GameTicking;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.Administration.Logs;
using Content.Shared.Chat;
using Content.Shared.Mind.Components;
namespace Content.Server.Chat
namespace Content.Server.Chat;
public sealed class SuicideSystem : EntitySystem
{
public sealed class SuicideSystem : EntitySystem
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
public override void Initialize()
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
base.Initialize();
public bool Suicide(EntityUid victim)
{
// Checks to see if the CannotSuicide tag exits, ghosts instead.
if (_tagSystem.HasTag(victim, "CannotSuicide"))
return false;
// Checks to see if the player is dead.
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
return false;
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
var suicideEvent = new SuicideEvent(victim);
//Check to see if there were any systems blocking this suicide
if (SuicideAttemptBlocked(victim, suicideEvent))
return false;
bool environmentSuicide = false;
// If you are critical, you wouldn't be able to use your surroundings to suicide, so you do the default suicide
if (!_mobState.IsCritical(victim, mobState))
{
environmentSuicide = EnvironmentSuicideHandler(victim, suicideEvent);
}
if (suicideEvent.AttemptBlocked)
return false;
DefaultSuicideHandler(victim, suicideEvent);
ApplyDeath(victim, suicideEvent.Kind!.Value);
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided{(environmentSuicide ? " (environment)" : "")}");
return true;
}
/// <summary>
/// If not handled, does the default suicide, which is biting your own tongue
/// </summary>
private void DefaultSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
{
if (suicideEvent.Handled)
return;
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
var selfMessage = Loc.GetString("suicide-command-default-text-self");
_popup.PopupEntity(selfMessage, victim, victim);
suicideEvent.SetHandled(SuicideKind.Bloodloss);
}
/// <summary>
/// Checks to see if there are any other systems that prevent suicide
/// </summary>
/// <returns>Returns true if there was a blocked attempt</returns>
private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent)
{
RaiseLocalEvent(victim, suicideEvent, true);
if (suicideEvent.AttemptBlocked)
return true;
SubscribeLocalEvent<DamageableComponent, SuicideEvent>(OnDamageableSuicide);
SubscribeLocalEvent<MobStateComponent, SuicideEvent>(OnEnvironmentalSuicide);
SubscribeLocalEvent<MindContainerComponent, SuicideGhostEvent>(OnSuicideGhost);
}
/// <summary>
/// Calling this function will attempt to kill the user by suiciding on objects in the surrounding area
/// or by applying a lethal amount of damage to the user with the default method.
/// Used when writing /suicide
/// </summary>
public bool Suicide(EntityUid victim)
{
// Can't suicide if we're already dead
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
return false;
}
/// <summary>
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
/// </summary>
private bool EnvironmentSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
{
var itemQuery = GetEntityQuery<ItemComponent>();
// Suicide by held item
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
&& handsComponent.ActiveHandEntity is { } item)
{
RaiseLocalEvent(item, suicideEvent, false);
if (suicideEvent.Handled)
return true;
}
// Suicide by nearby entity (ex: Microwave)
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
{
// Skip any nearby items that can be picked up, we already checked the active held item above
if (itemQuery.HasComponent(entity))
continue;
RaiseLocalEvent(entity, suicideEvent);
if (suicideEvent.Handled)
return true;
}
var suicideGhostEvent = new SuicideGhostEvent(victim);
RaiseLocalEvent(victim, suicideGhostEvent);
// Suicide is considered a fail if the user wasn't able to ghost
// Suiciding with the CannotSuicide tag will ghost the player but not kill the body
if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, "CannotSuicide"))
return false;
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
var suicideEvent = new SuicideEvent(victim);
RaiseLocalEvent(victim, suicideEvent);
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided.");
return true;
}
/// <summary>
/// Event subscription created to handle the ghosting aspect relating to suicides
/// Mainly useful when you can raise an event in Shared and can't call Suicide() directly
/// </summary>
private void OnSuicideGhost(Entity<MindContainerComponent> victim, ref SuicideGhostEvent args)
{
if (args.Handled)
return;
if (victim.Comp.Mind == null)
return;
if (!TryComp<MindComponent>(victim.Comp.Mind, out var mindComponent))
return;
// CannotSuicide tag will allow the user to ghost, but also return to their mind
// This is kind of weird, not sure what it applies to?
if (_tagSystem.HasTag(victim, "CannotSuicide"))
args.CanReturnToBody = true;
if (_gameTicker.OnGhostAttempt(victim.Comp.Mind.Value, args.CanReturnToBody, mind: mindComponent))
args.Handled = true;
}
/// <summary>
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
/// </summary>
private void OnEnvironmentalSuicide(Entity<MobStateComponent> victim, ref SuicideEvent args)
{
if (args.Handled || _mobState.IsCritical(victim))
return;
var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim);
// Try to suicide by raising an event on the held item
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
&& handsComponent.ActiveHandEntity is { } item)
{
RaiseLocalEvent(item, suicideByEnvironmentEvent);
if (suicideByEnvironmentEvent.Handled)
{
args.Handled = suicideByEnvironmentEvent.Handled;
return;
}
}
private void ApplyDeath(EntityUid target, SuicideKind kind)
// Try to suicide by nearby entities, like Microwaves or Crematoriums, by raising an event on it
// Returns upon being handled by any entity
var itemQuery = GetEntityQuery<ItemComponent>();
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
{
if (kind == SuicideKind.Special)
return;
// Skip any nearby items that can be picked up, we already checked the active held item above
if (itemQuery.HasComponent(entity))
continue;
if (!_prototypeManager.TryIndex<DamageTypePrototype>(kind.ToString(), out var damagePrototype))
{
const SuicideKind fallback = SuicideKind.Blunt;
Log.Error($"{nameof(SuicideSystem)} could not find the damage type prototype associated with {kind}. Falling back to {fallback}");
damagePrototype = _prototypeManager.Index<DamageTypePrototype>(fallback.ToString());
}
const int lethalAmountOfDamage = 200; // TODO: Would be nice to get this number from somewhere else
_damageableSystem.TryChangeDamage(target, new(damagePrototype, lethalAmountOfDamage), true, origin: target);
RaiseLocalEvent(entity, suicideByEnvironmentEvent);
if (!suicideByEnvironmentEvent.Handled)
continue;
args.Handled = suicideByEnvironmentEvent.Handled;
return;
}
}
/// <summary>
/// Default suicide behavior for any kind of entity that can take damage
/// </summary>
private void OnDamageableSuicide(Entity<DamageableComponent> victim, ref SuicideEvent args)
{
if (args.Handled)
return;
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
var selfMessage = Loc.GetString("suicide-command-default-text-self");
_popup.PopupEntity(selfMessage, victim, victim);
if (args.DamageSpecifier != null)
{
_suicide.ApplyLethalDamage(victim, args.DamageSpecifier);
args.Handled = true;
return;
}
args.DamageType ??= "Bloodloss";
_suicide.ApplyLethalDamage(victim, args.DamageType);
args.Handled = true;
}
}

View File

@@ -330,11 +330,41 @@ public sealed partial class ChatSystem : SharedChatSystem
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
_audio.PlayGlobal(announcementSound == null ? DefaultAnnouncementSound : _audio.GetSound(announcementSound), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
}
/// <summary>
/// Dispatches an announcement to players selected by filter.
/// </summary>
/// <param name="filter">Filter to select players who will recieve the announcement</param>
/// <param name="message">The contents of the message</param>
/// <param name="source">The entity making the announcement (used to determine the station)</param>
/// <param name="sender">The sender (Communications Console in Communications Console Announcement)</param>
/// <param name="playDefaultSound">Play the announcement sound</param>
/// <param name="announcementSound">Sound to play</param>
/// <param name="colorOverride">Optional color for the announcement message</param>
public void DispatchFilteredAnnouncement(
Filter filter,
string message,
EntityUid? source = null,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound?.ToString() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
}
/// <summary>
/// Dispatches an announcement on a specific station
/// </summary>
@@ -370,7 +400,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (playDefaultSound)
{
_audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
_audio.PlayGlobal(announcementSound?.ToString() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");

View File

@@ -123,7 +123,7 @@ namespace Content.Server.Chemistry.EntitySystems
var reagent = _protoManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
var reaction =
reagent.ReactionTile(tile, (reagentQuantity.Quantity / vapor.TransferAmount) * 0.25f, EntityManager);
reagent.ReactionTile(tile, (reagentQuantity.Quantity / vapor.TransferAmount) * 0.25f, EntityManager, reagentQuantity.Reagent.Data);
if (reaction > reagentQuantity.Quantity)
{

View File

@@ -21,10 +21,12 @@ public sealed partial class CleanDecalsReaction : ITileReaction
[DataField]
public FixedPoint2 CleanCost { get; private set; } = FixedPoint2.New(0.25f);
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
if (reactVolume <= CleanCost ||
!entityManager.TryGetComponent<MapGridComponent>(tile.GridUid, out var grid) ||

View File

@@ -34,7 +34,8 @@ public sealed partial class CleanTileReaction : ITileReaction
FixedPoint2 ITileReaction.TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager
, List<ReagentData>? data)
{
var entities = entityManager.System<EntityLookupSystem>().GetLocalEntitiesIntersecting(tile, 0f).ToArray();
var puddleQuery = entityManager.GetEntityQuery<PuddleComponent>();

View File

@@ -38,7 +38,8 @@ public sealed partial class CreateEntityTileReaction : ITileReaction
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
if (reactVolume >= Usage)
{

View File

@@ -17,7 +17,8 @@ namespace Content.Server.Chemistry.TileReactions
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
if (reactVolume <= FixedPoint2.Zero || tile.Tile.IsEmpty)
return FixedPoint2.Zero;

View File

@@ -16,7 +16,8 @@ namespace Content.Server.Chemistry.TileReactions
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
if (reactVolume <= FixedPoint2.Zero || tile.Tile.IsEmpty)
return FixedPoint2.Zero;

View File

@@ -1,4 +1,4 @@
using Content.Server.Maps;
using Content.Server.Maps;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
@@ -15,7 +15,8 @@ public sealed partial class PryTileReaction : ITileReaction
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
var sys = entityManager.System<TileSystem>();
sys.PryTile(tile);

View File

@@ -15,13 +15,14 @@ namespace Content.Server.Chemistry.TileReactions
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
var spillSystem = entityManager.System<PuddleSystem>();
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _))
return FixedPoint2.Zero;
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out _, sound: false, tileReact: false)
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out _, sound: false, tileReact: false)
? reactVolume
: FixedPoint2.Zero;
}

View File

@@ -29,13 +29,14 @@ namespace Content.Server.Chemistry.TileReactions
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager)
IEntityManager entityManager,
List<ReagentData>? data)
{
if (reactVolume < 5)
return FixedPoint2.Zero;
if (entityManager.EntitySysManager.GetEntitySystem<PuddleSystem>()
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false))
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out var puddleUid, false, false))
{
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;

View File

@@ -1,12 +0,0 @@
namespace Content.Server.Clothing.Components;
/// <summary>
/// TODO this needs removed somehow.
/// Handles 'heat resistance' for gloves touching bulbs and that's it, ick.
/// </summary>
[RegisterComponent]
public sealed partial class GloveHeatResistanceComponent : Component
{
[DataField("heatResistance")]
public int HeatResistance = 323;
}

View File

@@ -0,0 +1,92 @@
using Content.Server.Administration.Logs;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Server.NPC;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Popups;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Database;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Players;
using Content.Shared.Popups;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Clothing.Systems;
/// <inheritdoc/>
public sealed class CursedMaskSystem : SharedCursedMaskSystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly PopupSystem _popup = default!;
// We can't store this info on the component easily
private static readonly ProtoId<HTNCompoundPrototype> TakeoverRootTask = "SimpleHostileCompound";
protected override void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer)
{
if (ent.Comp.CurrentState != CursedMaskExpression.Anger)
return;
if (TryComp<ActorComponent>(wearer, out var actor) && actor.PlayerSession.GetMind() is { } mind)
{
var session = actor.PlayerSession;
if (!_ticker.OnGhostAttempt(mind, false))
return;
ent.Comp.StolenMind = mind;
_popup.PopupEntity(Loc.GetString("cursed-mask-takeover-popup"), wearer, session, PopupType.LargeCaution);
_adminLog.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(wearer):player} had their body taken over and turned into an enemy through the cursed mask {ToPrettyString(ent):entity}");
}
var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer);
ent.Comp.OldFactions = npcFaction.Factions;
_npcFaction.ClearFactions((wearer, npcFaction), false);
_npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction);
ent.Comp.HasNpc = !EnsureComp<HTNComponent>(wearer, out var htn);
htn.RootTask = new HTNCompoundTask { Task = TakeoverRootTask };
htn.Blackboard.SetValue(NPCBlackboard.Owner, wearer);
_npc.WakeNPC(wearer, htn);
_htn.Replan(htn);
}
protected override void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args)
{
// If we are taking off the cursed mask
if (ent.Comp.CurrentState == CursedMaskExpression.Anger)
{
if (ent.Comp.HasNpc)
RemComp<HTNComponent>(args.Wearer);
var npcFaction = EnsureComp<NpcFactionMemberComponent>(args.Wearer);
_npcFaction.RemoveFaction((args.Wearer, npcFaction), ent.Comp.CursedMaskFaction, false);
_npcFaction.AddFactions((args.Wearer, npcFaction), ent.Comp.OldFactions);
ent.Comp.HasNpc = false;
ent.Comp.OldFactions.Clear();
if (Exists(ent.Comp.StolenMind))
{
_mind.TransferTo(ent.Comp.StolenMind.Value, args.Wearer);
_adminLog.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(args.Wearer):player} was restored to their body after the removal of {ToPrettyString(ent):entity}.");
ent.Comp.StolenMind = null;
}
}
RandomizeCursedMask(ent, args.Wearer);
}
}

View File

@@ -212,7 +212,7 @@ namespace Content.Server.Connection
var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var validAccountAge = record != null &&
record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId);
// Use the custom reason if it exists & they don't have the minimum account age
@@ -307,7 +307,7 @@ namespace Content.Server.Connection
if (record == null)
return (false, "");
var isAccountAgeInvalid = record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
var isAccountAgeInvalid = record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
if (isAccountAgeInvalid)
{

View File

@@ -0,0 +1,92 @@
using Content.Server.Destructible;
using Content.Shared.Construction;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Content.Server.Construction.Conditions;
/// <summary>
/// Requires that the structure has at least some amount of health
/// </summary>
[DataDefinition]
public sealed partial class MinHealth : IGraphCondition
{
/// <summary>
/// If ByProportion is true, Threshold is a value less than or equal to 1, but more than 0,
/// which is compared to the percent of health remaining in the structure.
/// Else, Threshold is any positive value with at most 2 decimal points of percision,
/// which is compared to the current health of the structure.
/// </summary>
[DataField]
public FixedPoint2 Threshold = 1;
[DataField]
public bool ByProportion = false;
[DataField]
public bool IncludeEquals = true;
public bool Condition(EntityUid uid, IEntityManager entMan)
{
if (!entMan.TryGetComponent(uid, out DestructibleComponent? destructibleComp) ||
!entMan.TryGetComponent(uid, out DamageableComponent? damageComp))
{
return false;
}
var destructionSys = entMan.System<DestructibleSystem>();
var maxHealth = destructionSys.DestroyedAt(uid, destructibleComp);
var curHealth = maxHealth - damageComp.TotalDamage;
var proportionHealth = curHealth / maxHealth;
if (IncludeEquals)
{
if (ByProportion)
{
return proportionHealth >= Threshold;
}
else
{
return curHealth >= Threshold;
}
}
else
{
if (ByProportion)
{
return proportionHealth > Threshold;
}
else
{
return curHealth > Threshold;
}
}
}
public bool DoExamine(ExaminedEvent args)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var entity = args.Examined;
if (Condition(entity, entMan))
{
return false;
}
args.PushMarkup(Loc.GetString("construction-examine-condition-low-health"));
return true;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry()
{
Localization = "construction-step-condition-low-health"
};
}
}

View File

@@ -287,7 +287,7 @@ namespace Content.Server.Construction
{
BreakOnDamage = false,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
};
var started = _doAfterSystem.TryStartDoAfter(doAfterEventArgs);

View File

@@ -17,8 +17,13 @@ namespace Content.Server.Disposal
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player?.AttachedEntity == null)
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
if (player.AttachedEntity is not { } attached)
{
shell.WriteLine(Loc.GetString("shell-only-players-can-run-this-command"));
return;

View File

@@ -488,7 +488,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = false
NeedHand = false,
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);

View File

@@ -128,7 +128,7 @@ public sealed partial class EnsnareableSystem
/// <param name="component">The ensnaring component</param>
public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, EnsnaringComponent component)
{
//Don't do anything if they don't have the ensnareable component.
// Don't do anything if they don't have the ensnareable component.
if (!HasComp<EnsnareableComponent>(target))
return;
@@ -140,7 +140,7 @@ public sealed partial class EnsnareableSystem
BreakOnMove = breakOnMove,
BreakOnDamage = false,
NeedHand = true,
BlockDuplicate = true,
BreakOnDropItem = false,
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))

View File

@@ -23,13 +23,13 @@ namespace Content.Server.EntityList
if (shell.Player is not { } player)
{
shell.WriteError("You must be a player to run this command.");
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
if (player.AttachedEntity is not {} attached)
{
shell.WriteError("You must have an entity to run this command.");
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
return;
}

View File

@@ -1,7 +1,10 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Explosion.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.EntitySystems;
@@ -14,6 +17,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -205,7 +209,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
MapCoordinates epicenter,
HashSet<EntityUid> processed,
string id,
float? fireStacks)
float? fireStacks,
EntityUid? cause)
{
var size = grid.Comp.TileSize;
var gridBox = new Box2(tile * size, (tile + 1) * size);
@@ -224,7 +229,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
// process those entities
foreach (var (uid, xform) in list)
{
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks);
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
// process anchored entities
@@ -234,7 +239,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
foreach (var entity in _anchored)
{
processed.Add(entity);
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks);
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks, cause);
}
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
@@ -270,7 +275,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
{
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
// they should handle their own damage pass-through, with their own damage reduction calculation.
ProcessEntity(uid, epicenter, null, throwForce, id, xform, null);
ProcessEntity(uid, epicenter, null, throwForce, id, xform, null, cause);
}
return !tileBlocked;
@@ -306,7 +311,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
MapCoordinates epicenter,
HashSet<EntityUid> processed,
string id,
float? fireStacks)
float? fireStacks,
EntityUid? cause)
{
var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize));
var worldBox = spaceMatrix.TransformBox(gridBox);
@@ -322,7 +328,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
foreach (var (uid, xform) in state.Item1)
{
processed.Add(uid);
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks);
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
if (throwForce <= 0)
@@ -336,7 +342,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
foreach (var (uid, xform) in list)
{
ProcessEntity(uid, epicenter, null, throwForce, id, xform, fireStacks);
ProcessEntity(uid, epicenter, null, throwForce, id, xform, fireStacks, cause);
}
}
@@ -434,13 +440,28 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
float throwForce,
string id,
TransformComponent? xform,
float? fireStacksOnIgnite)
float? fireStacksOnIgnite,
EntityUid? cause)
{
if (originalDamage != null)
{
GetEntitiesToDamage(uid, originalDamage, id);
foreach (var (entity, damage) in _toDamage)
{
if (damage.GetTotal() > 0 && TryComp<ActorComponent>(entity, out var actorComponent))
{
// Log damage to player entities only, cause this will create a massive amount of log spam otherwise.
if (cause != null)
{
_adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
}
else
{
_adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
}
}
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
@@ -647,6 +668,8 @@ sealed class Explosion
public readonly EntityUid VisualEnt;
public readonly EntityUid? Cause;
/// <summary>
/// Initialize a new instance for processing
/// </summary>
@@ -663,9 +686,11 @@ sealed class Explosion
bool canCreateVacuum,
IEntityManager entMan,
IMapManager mapMan,
EntityUid visualEnt)
EntityUid visualEnt,
EntityUid? cause)
{
VisualEnt = visualEnt;
Cause = cause;
_system = system;
ExplosionType = explosionType;
_tileSetIntensity = tileSetIntensity;
@@ -829,7 +854,8 @@ sealed class Explosion
Epicenter,
ProcessedEntities,
ExplosionType.ID,
ExplosionType.FireStacks);
ExplosionType.FireStacks,
Cause);
// If the floor is not blocked by some dense object, damage the floor tiles.
if (canDamageFloor)
@@ -847,7 +873,8 @@ sealed class Explosion
Epicenter,
ProcessedEntities,
ExplosionType.ID,
ExplosionType.FireStacks);
ExplosionType.FireStacks,
Cause);
}
if (!MoveNext())
@@ -888,4 +915,5 @@ public sealed class QueuedExplosion
public float TotalIntensity, Slope, MaxTileIntensity, TileBreakScale;
public int MaxTileBreak;
public bool CanCreateVacuum;
public EntityUid? Cause; // The entity that exploded, for logging purposes.
}

View File

@@ -253,7 +253,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var posFound = _transformSystem.TryGetMapOrGridCoordinates(uid, out var gridPos, pos);
QueueExplosion(mapPos, typeId, totalIntensity, slope, maxTileIntensity, tileBreakScale, maxTileBreak, canCreateVacuum, addLog: false);
QueueExplosion(mapPos, typeId, totalIntensity, slope, maxTileIntensity, uid, tileBreakScale, maxTileBreak, canCreateVacuum, addLog: false);
if (!addLog)
return;
@@ -281,6 +281,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
float totalIntensity,
float slope,
float maxTileIntensity,
EntityUid? cause,
float tileBreakScale = 1f,
int maxTileBreak = int.MaxValue,
bool canCreateVacuum = true,
@@ -324,7 +325,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
MaxTileIntensity = maxTileIntensity,
TileBreakScale = tileBreakScale,
MaxTileBreak = maxTileBreak,
CanCreateVacuum = canCreateVacuum
CanCreateVacuum = canCreateVacuum,
Cause = cause
};
_explosionQueue.Enqueue(boom);
_queuedExplosions.Add(boom);
@@ -393,7 +395,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
queued.CanCreateVacuum,
EntityManager,
_mapManager,
visualEnt);
visualEnt,
queued.Cause);
}
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)

View File

@@ -38,16 +38,20 @@ public sealed partial class TriggerSystem
Trigger(uid);
}
/// <summary>
/// Checks if the user has any implants that prevent suicide to avoid some cheesy strategies
/// Prevents suicide by handling the event without killing the user
/// </summary>
private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component, SuicideEvent args)
{
if (args.Handled)
return;
if (component.PreventSuicide)
{
_popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
args.BlockSuicideAttempt(component.PreventSuicide);
}
if (!component.PreventSuicide)
return;
_popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
args.Handled = true;
}
private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args)

View File

@@ -1,9 +1,9 @@
using Content.Server.Explosion.Components;
using Content.Server.Sticky.Events;
using Content.Shared.Examine;
using Content.Shared.Explosion.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Sticky;
using Content.Shared.Verbs;
namespace Content.Server.Explosion.EntitySystems;
@@ -21,7 +21,7 @@ public sealed partial class TriggerSystem
SubscribeLocalEvent<RandomTimerTriggerComponent, MapInitEvent>(OnRandomTimerTriggerMapInit);
}
private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, EntityStuckEvent args)
private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, ref EntityStuckEvent args)
{
if (!component.StartOnStick)
return;

View File

@@ -52,16 +52,6 @@ public sealed class FaxSystem : EntitySystem
private const string PaperSlotId = "Paper";
/// <summary>
/// The prototype ID to use for faxed or copied entities if we can't get one from
/// the paper entity for whatever reason.
/// </summary>
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultPaperPrototypeId = "Paper";
[ValidatePrototypeId<EntityPrototype>]
private const string OfficePaperPrototypeId = "PaperOffice";
public override void Initialize()
{
base.Initialize();
@@ -242,7 +232,8 @@ public sealed class FaxSystem : EntitySystem
return;
}
_adminLogger.Add(LogType.Action, LogImpact.Low,
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.User):user} renamed {ToPrettyString(uid):tool} from \"{component.FaxName}\" to \"{newName}\"");
component.FaxName = newName;
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-set"), uid);
@@ -324,7 +315,7 @@ public sealed class FaxSystem : EntitySystem
private void OnCopyButtonPressed(EntityUid uid, FaxMachineComponent component, FaxCopyMessage args)
{
if (HasComp<MobStateComponent>(component.PaperSlot.Item))
_faxecute.Faxecute(uid, component); /// when button pressed it will hurt the mob.
_faxecute.Faxecute(uid, component); // when button pressed it will hurt the mob.
else
Copy(uid, component, args);
}
@@ -332,7 +323,7 @@ public sealed class FaxSystem : EntitySystem
private void OnSendButtonPressed(EntityUid uid, FaxMachineComponent component, FaxSendMessage args)
{
if (HasComp<MobStateComponent>(component.PaperSlot.Item))
_faxecute.Faxecute(uid, component); /// when button pressed it will hurt the mob.
_faxecute.Faxecute(uid, component); // when button pressed it will hurt the mob.
else
Send(uid, component, args);
}
@@ -425,11 +416,7 @@ public sealed class FaxSystem : EntitySystem
/// </summary>
public void PrintFile(EntityUid uid, FaxMachineComponent component, FaxFileMessage args)
{
string prototype;
if (args.OfficePaper)
prototype = OfficePaperPrototypeId;
else
prototype = DefaultPaperPrototypeId;
var prototype = args.OfficePaper ? component.PrintOfficePaperId : component.PrintPaperId;
var name = Loc.GetString("fax-machine-printed-paper-name");
@@ -441,7 +428,8 @@ public sealed class FaxSystem : EntitySystem
// Unfortunately, since a paper entity does not yet exist, we have to emulate what LabelSystem will do.
var nameWithLabel = (args.Label is { } label) ? $"{name} ({label})" : name;
_adminLogger.Add(LogType.Action, LogImpact.Low,
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +
$"added print job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"of {nameWithLabel}: {args.Content}");
@@ -471,7 +459,7 @@ public sealed class FaxSystem : EntitySystem
var printout = new FaxPrintout(paper.Content,
nameMod?.BaseName ?? metadata.EntityName,
labelComponent?.CurrentLabel,
metadata.EntityPrototype?.ID ?? DefaultPaperPrototypeId,
metadata.EntityPrototype?.ID ?? component.PrintPaperId,
paper.StampState,
paper.StampedBy,
paper.EditingDisabled);
@@ -484,7 +472,8 @@ public sealed class FaxSystem : EntitySystem
UpdateUserInterface(uid, component);
_adminLogger.Add(LogType.Action, LogImpact.Low,
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +
$"added copy job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"of {ToPrettyString(sendEntity):subject}: {printout.Content}");
@@ -531,7 +520,7 @@ public sealed class FaxSystem : EntitySystem
// TODO: Ideally, we could just make a copy of the whole entity when it's
// faxed, in order to preserve visuals, etc.. This functionality isn't
// available yet, so we'll pass along the originating prototypeId and fall
// back to DefaultPaperPrototypeId in SpawnPaperFromQueue if we can't find one here.
// back to component.PrintPaperId in SpawnPaperFromQueue if we can't find one here.
payload[FaxConstants.FaxPaperPrototypeData] = metadata.EntityPrototype.ID;
}
@@ -543,7 +532,8 @@ public sealed class FaxSystem : EntitySystem
_deviceNetworkSystem.QueuePacket(uid, component.DestinationFaxAddress, payload);
_adminLogger.Add(LogType.Action, LogImpact.Low,
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +
$"sent fax from \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"to \"{faxName}\" ({component.DestinationFaxAddress}) " +
@@ -585,7 +575,7 @@ public sealed class FaxSystem : EntitySystem
var printout = component.PrintingQueue.Dequeue();
var entityToSpawn = printout.PrototypeId.Length == 0 ? DefaultPaperPrototypeId : printout.PrototypeId;
var entityToSpawn = printout.PrototypeId.Length == 0 ? component.PrintPaperId.ToString() : printout.PrototypeId;
var printed = EntityManager.SpawnEntity(entityToSpawn, Transform(uid).Coordinates);
if (TryComp<PaperComponent>(printed, out var paper))

View File

@@ -712,7 +712,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
{
var (reagent, quantity) = solution.Contents[i];
var proto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
var removed = proto.ReactionTile(tileRef, quantity, EntityManager);
var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data);
if (removed <= FixedPoint2.Zero)
continue;

View File

@@ -315,7 +315,7 @@ public sealed class SmokeSystem : EntitySystem
continue;
var reagent = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
reagent.ReactionTile(tile, reagentQuantity.Quantity, EntityManager);
reagent.ReactionTile(tile, reagentQuantity.Quantity, EntityManager, reagentQuantity.Reagent.Data);
}
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Forensics;
/// <summary>
/// This component stops the entity from leaving finger prints,
/// usually so fibres can be left instead.
/// </summary>
[RegisterComponent]
public sealed partial class DnaSubstanceTraceComponent : Component
{ }

View File

@@ -26,7 +26,13 @@ namespace Content.Server.Forensics
/// DNA that the forensic scanner found from the <see cref="DNAComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("dnas")]
public List<string> DNAs = new();
public List<string> TouchDNAs = new();
/// <summary>
/// DNA that the forensic scanner found from the solution containers in an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField]
public List<string> SolutionDNAs = new();
/// <summary>
/// Residue that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.

View File

@@ -3,16 +3,19 @@ using System.Text;
using Content.Server.Popups;
using Content.Shared.UserInterface;
using Content.Shared.DoAfter;
using Content.Shared.Fluids.Components;
using Content.Shared.Forensics;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Paper;
using Content.Shared.Verbs;
using Content.Shared.Tag;
using Robust.Shared.Audio.Systems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Content.Server.Chemistry.Containers.EntitySystems;
// todo: remove this stinky LINQy
namespace Content.Server.Forensics
@@ -27,6 +30,9 @@ namespace Content.Server.Forensics
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
@@ -46,7 +52,8 @@ namespace Content.Server.Forensics
var state = new ForensicScannerBoundUserInterfaceState(
component.Fingerprints,
component.Fibers,
component.DNAs,
component.TouchDNAs,
component.SolutionDNAs,
component.Residues,
component.LastScannedName,
component.PrintCooldown,
@@ -69,18 +76,25 @@ namespace Content.Server.Forensics
{
scanner.Fingerprints = new();
scanner.Fibers = new();
scanner.DNAs = new();
scanner.TouchDNAs = new();
scanner.Residues = new();
}
else
{
scanner.Fingerprints = forensics.Fingerprints.ToList();
scanner.Fibers = forensics.Fibers.ToList();
scanner.DNAs = forensics.DNAs.ToList();
scanner.TouchDNAs = forensics.DNAs.ToList();
scanner.Residues = forensics.Residues.ToList();
}
if (_tag.HasTag(args.Args.Target.Value, "DNASolutionScannable"))
{
scanner.SolutionDNAs = _forensicsSystem.GetSolutionsDNA(args.Args.Target.Value);
} else
{
scanner.SolutionDNAs = new();
}
scanner.LastScannedName = MetaData(args.Args.Target.Value).EntityName;
}
@@ -206,10 +220,17 @@ namespace Content.Server.Forensics
}
text.AppendLine();
text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas"));
foreach (var dna in component.DNAs)
foreach (var dna in component.TouchDNAs)
{
text.AppendLine(dna);
}
foreach (var dna in component.SolutionDNAs)
{
Log.Debug(dna);
if (component.TouchDNAs.Contains(dna))
continue;
text.AppendLine(dna);
}
text.AppendLine();
text.AppendLine(Loc.GetString("forensic-scanner-interface-residues"));
foreach (var residue in component.Residues)
@@ -232,7 +253,8 @@ namespace Content.Server.Forensics
{
component.Fingerprints = new();
component.Fibers = new();
component.DNAs = new();
component.TouchDNAs = new();
component.SolutionDNAs = new();
component.LastScannedName = string.Empty;
UpdateUserInterface(uid, component);

View File

@@ -1,11 +1,16 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.DoAfter;
using Content.Shared.Fluids.Components;
using Content.Shared.Forensics;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
@@ -23,20 +28,35 @@ namespace Content.Server.Forensics
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<DnaComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<ForensicsComponent, GotRehydratedEvent>(OnRehydrated);
SubscribeLocalEvent<CleansForensicsComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) });
SubscribeLocalEvent<ForensicsComponent, CleanForensicsDoAfterEvent>(OnCleanForensicsDoAfter);
SubscribeLocalEvent<DnaComponent, TransferDnaEvent>(OnTransferDnaEvent);
SubscribeLocalEvent<DnaSubstanceTraceComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<CleansForensicsComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}
private void OnSolutionChanged(Entity<DnaSubstanceTraceComponent> ent, ref SolutionContainerChangedEvent ev)
{
var soln = GetSolutionsDNA(ev.Solution);
if (soln.Count > 0)
{
var comp = EnsureComp<ForensicsComponent>(ent.Owner);
foreach (string dna in soln)
{
comp.DNAs.Add(dna);
}
}
}
private void OnInteract(EntityUid uid, FingerprintComponent component, ContactInteractionEvent args)
@@ -51,15 +71,26 @@ namespace Content.Server.Forensics
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
{
component.DNA = GenerateDNA();
if (component.DNA == String.Empty)
{
component.DNA = GenerateDNA();
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
RaiseLocalEvent(uid, ref ev);
}
}
private void OnBeingGibbed(EntityUid uid, DnaComponent component, BeingGibbedEvent args)
private void OnBeingGibbed(EntityUid uid, ForensicsComponent component, BeingGibbedEvent args)
{
string dna = Loc.GetString("forensics-dna-unknown");
if (TryComp(uid, out DnaComponent? dnaComp))
dna = dnaComp.DNA;
foreach (EntityUid part in args.GibbedParts)
{
var partComp = EnsureComp<ForensicsComponent>(part);
partComp.DNAs.Add(component.DNA);
partComp.DNAs.Add(dna);
partComp.CanDnaBeCleaned = false;
}
}
@@ -106,6 +137,34 @@ namespace Content.Server.Forensics
}
}
public List<string> GetSolutionsDNA(EntityUid uid)
{
List<string> list = new();
if (TryComp<SolutionContainerManagerComponent>(uid, out var comp))
{
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((uid, comp)))
{
list.AddRange(GetSolutionsDNA(soln.Comp.Solution));
}
}
return list;
}
public List<string> GetSolutionsDNA(Solution soln)
{
List<string> list = new();
foreach (var reagent in soln.Contents)
{
foreach (var data in reagent.Reagent.EnsureReagentData())
{
if (data is DnaData)
{
list.Add(((DnaData) data).DNA);
}
}
}
return list;
}
private void OnAfterInteract(Entity<CleansForensicsComponent> cleanForensicsEntity, ref AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target == null)
@@ -159,7 +218,6 @@ namespace Content.Server.Forensics
var cleanDelay = cleanForensicsEntity.Comp.CleanDelay;
var doAfterArgs = new DoAfterArgs(EntityManager, user, cleanDelay, new CleanForensicsDoAfterEvent(), cleanForensicsEntity, target: target, used: cleanForensicsEntity)
{
BreakOnHandChange = true,
NeedHand = true,
BreakOnDamage = true,
BreakOnMove = true,

View File

@@ -19,6 +19,7 @@ namespace Content.Server.GameTicking.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -179,5 +179,11 @@ namespace Content.Server.GameTicking
// update server info to reflect new ready count
UpdateInfoText();
}
public bool UserHasJoinedGame(ICommonSession session)
=> UserHasJoinedGame(session.UserId);
public bool UserHasJoinedGame(NetUserId userId)
=> PlayerGameStatuses[userId] == PlayerGameStatus.JoinedGame;
}
}

View File

@@ -1,18 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.Polymorph;
using Robust.Shared.Prototypes;
namespace Content.Server.Geras;
/// <summary>
/// This component assigns the entity with a polymorph action.
/// </summary>
[RegisterComponent]
public sealed partial class GerasComponent : Component
{
[DataField] public ProtoId<PolymorphPrototype> GerasPolymorphId = "SlimeMorphGeras";
[DataField] public EntProtoId GerasAction = "ActionMorphGeras";
[DataField] public EntityUid? GerasActionEntity;
}

View File

@@ -1,51 +0,0 @@
using Content.Server.Polymorph.Systems;
using Content.Shared.Zombies;
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Shared.Geras;
using Robust.Shared.Player;
namespace Content.Server.Geras;
/// <inheritdoc/>
public sealed class GerasSystem : SharedGerasSystem
{
[Dependency] private readonly PolymorphSystem _polymorphSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<GerasComponent, MorphIntoGeras>(OnMorphIntoGeras);
SubscribeLocalEvent<GerasComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GerasComponent, EntityZombifiedEvent>(OnZombification);
}
private void OnZombification(EntityUid uid, GerasComponent component, EntityZombifiedEvent args)
{
_actionsSystem.RemoveAction(uid, component.GerasActionEntity);
}
private void OnMapInit(EntityUid uid, GerasComponent component, MapInitEvent args)
{
// try to add geras action
_actionsSystem.AddAction(uid, ref component.GerasActionEntity, component.GerasAction);
}
private void OnMorphIntoGeras(EntityUid uid, GerasComponent component, MorphIntoGeras args)
{
if (HasComp<ZombieComponent>(uid))
return; // i hate zomber.
var ent = _polymorphSystem.PolymorphEntity(uid, component.GerasPolymorphId);
if (!ent.HasValue)
return;
_popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-others", ("entity", ent.Value)), ent.Value, Filter.PvsExcept(ent.Value), true);
_popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-user"), ent.Value, ent.Value);
args.Handled = true;
}
}

View File

@@ -1,10 +1,11 @@
using Content.Server.Cuffs;
using Content.Server.Cuffs;
using Content.Server.Forensics;
using Content.Server.Humanoid;
using Content.Server.Implants.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Forensics;
using Content.Shared.Humanoid;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
@@ -212,6 +213,9 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
if (TryComp<DnaComponent>(ent, out var dna))
{
dna.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
RaiseLocalEvent(ent, ref ev);
}
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
{

View File

@@ -1,34 +1,22 @@
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Jobs
namespace Content.Server.Jobs;
public sealed partial class AddComponentSpecial : JobSpecial
{
[UsedImplicitly]
public sealed partial class AddComponentSpecial : JobSpecial
[DataField(required: true)]
public ComponentRegistry Components { get; private set; } = new();
/// <summary>
/// If this is true then existing components will be removed and replaced with these ones.
/// </summary>
[DataField]
public bool RemoveExisting = true;
public override void AfterEquip(EntityUid mob)
{
[DataField("components")]
[AlwaysPushInheritance]
public ComponentRegistry Components { get; private set; } = new();
public override void AfterEquip(EntityUid mob)
{
// now its a registry of components, still throws i bet.
// TODO: This is hot garbage and probably needs an engine change to not be a POS.
var factory = IoCManager.Resolve<IComponentFactory>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var serializationManager = IoCManager.Resolve<ISerializationManager>();
foreach (var (name, data) in Components)
{
var component = (Component) factory.GetComponent(name);
var temp = (object)component;
serializationManager.CopyTo(data.Component, ref temp);
entityManager.RemoveComponent(mob, temp!.GetType());
entityManager.AddComponent(mob, (Component)temp);
}
}
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.AddComponents(mob, Components, removeExisting: RemoveExisting);
}
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Jobs;
public sealed partial class RemoveComponentSpecial : JobSpecial
{
[DataField(required: true)]
public ComponentRegistry Components { get; private set; } = new();
public override void AfterEquip(EntityUid mob)
{
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.RemoveComponents(mob, Components);
}
}

View File

@@ -2,6 +2,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
@@ -16,7 +18,6 @@ using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -36,6 +37,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
public override void Initialize()
{
@@ -48,31 +50,38 @@ namespace Content.Server.Kitchen.EntitySystems
//DoAfter
SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop);
}
private void OnButcherableCanDrop(EntityUid uid, ButcherableComponent component, ref CanDropDraggedEvent args)
private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args)
{
args.Handled = true;
args.CanDrop |= component.Type != ButcheringType.Knife;
args.CanDrop |= entity.Comp.Type != ButcheringType.Knife;
}
private void OnSuicide(EntityUid uid, KitchenSpikeComponent component, SuicideEvent args)
/// <summary>
/// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them.
/// </summary>
private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Piercing);
var victim = args.Victim;
var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", victim));
_popupSystem.PopupEntity(othersMessage, victim);
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing");
var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", args.Victim));
_popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true);
var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self");
_popupSystem.PopupEntity(selfMessage, victim, victim);
_popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim);
args.Handled = true;
}
private void OnDoAfter(EntityUid uid, KitchenSpikeComponent component, DoAfterEvent args)
private void OnDoAfter(Entity<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args)
{
if (args.Args.Target == null)
return;
@@ -82,49 +91,49 @@ namespace Content.Server.Kitchen.EntitySystems
if (args.Cancelled)
{
component.InUse = false;
entity.Comp.InUse = false;
return;
}
if (args.Handled)
return;
if (Spikeable(uid, args.Args.User, args.Args.Target.Value, component, butcherable))
Spike(uid, args.Args.User, args.Args.Target.Value, component);
if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable))
Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp);
component.InUse = false;
entity.Comp.InUse = false;
args.Handled = true;
}
private void OnDragDrop(EntityUid uid, KitchenSpikeComponent component, ref DragDropTargetEvent args)
private void OnDragDrop(Entity<KitchenSpikeComponent> entity, ref DragDropTargetEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (Spikeable(uid, args.User, args.Dragged, component))
TrySpike(uid, args.User, args.Dragged, component);
if (Spikeable(entity, args.User, args.Dragged, entity.Comp))
TrySpike(entity, args.User, args.Dragged, entity.Comp);
}
private void OnInteractHand(EntityUid uid, KitchenSpikeComponent component, InteractHandEvent args)
private void OnInteractHand(Entity<KitchenSpikeComponent> entity, ref InteractHandEvent args)
{
if (args.Handled)
return;
if (component.PrototypesToSpawn?.Count > 0)
if (entity.Comp.PrototypesToSpawn?.Count > 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User);
args.Handled = true;
}
}
private void OnInteractUsing(EntityUid uid, KitchenSpikeComponent component, InteractUsingEvent args)
private void OnInteractUsing(Entity<KitchenSpikeComponent> entity, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (TryGetPiece(uid, args.User, args.Used))
if (TryGetPiece(entity, args.User, args.Used))
args.Handled = true;
}
@@ -259,7 +268,8 @@ namespace Content.Server.Kitchen.EntitySystems
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
BreakOnDropItem = false,
};
_doAfter.TryStartDoAfter(doAfterArgs);

View File

@@ -39,6 +39,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.Stacks;
using Content.Server.Construction.Components;
using Content.Shared.Chat;
using Content.Shared.Damage;
namespace Content.Server.Kitchen.EntitySystems
{
@@ -65,6 +67,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string MalfunctionSpark = "Spark";
@@ -83,7 +86,7 @@ namespace Content.Server.Kitchen.EntitySystems
SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MicrowaveComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<MicrowaveComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MicrowaveComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<MicrowaveComponent, SignalReceivedEvent>(OnSignalReceived);
@@ -260,12 +263,22 @@ namespace Content.Server.Kitchen.EntitySystems
_deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort);
}
private void OnSuicide(Entity<MicrowaveComponent> ent, ref SuicideEvent args)
/// <summary>
/// Kills the user by microwaving their head
/// TODO: Make this not awful, it keeps any items attached to your head still on and you can revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen.
/// </summary>
private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Heat);
// The act of getting your head microwaved doesn't actually kill you
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
// The application of lethal damage is what kills you...
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat");
var victim = args.Victim;
var headCount = 0;
@@ -295,6 +308,7 @@ namespace Content.Server.Kitchen.EntitySystems
ent.Comp.CurrentCookTimerTime = 10;
Wzhzhzh(ent.Owner, ent.Comp, args.Victim);
UpdateUserInterfaceState(ent.Owner, ent.Comp);
args.Handled = true;
}
private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionContainerChangedEvent args)

View File

@@ -77,7 +77,7 @@ public sealed class SharpSystem : EntitySystem
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
};
_doAfterSystem.TryStartDoAfter(doAfter);
return true;

View File

@@ -30,10 +30,6 @@ namespace Content.Server.Light.Components
[DataField("on")]
public bool On = true;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
[DataField("ignoreGhostsBoo")]
public bool IgnoreGhostsBoo;

View File

@@ -1,5 +1,4 @@
using Content.Server.Administration.Logs;
using Content.Server.Clothing.Components;
using Content.Server.DeviceLinking.Events;
using Content.Server.DeviceLinking.Systems;
using Content.Server.DeviceNetwork;
@@ -24,6 +23,8 @@ using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Components;
namespace Content.Server.Light.EntitySystems
{
@@ -33,11 +34,8 @@ namespace Content.Server.Light.EntitySystems
public sealed class PoweredLightSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
[Dependency] private readonly LightBulbSystem _bulbSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -45,7 +43,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PointLightSystem _pointLight = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly DamageOnInteractSystem _damageOnInteractSystem = default!;
private static readonly TimeSpan ThunkDelay = TimeSpan.FromSeconds(2);
public const string LightBulbContainer = "light_bulb";
@@ -106,40 +104,7 @@ namespace Content.Server.Light.EntitySystems
if (bulbUid == null)
return;
// check if it's possible to apply burn damage to user
var userUid = args.User;
if (EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
{
// get users heat resistance
var res = int.MinValue;
if (_inventory.TryGetSlotEntity(userUid, "gloves", out var slotEntity) &&
TryComp<GloveHeatResistanceComponent>(slotEntity, out var gloves))
{
res = gloves.HeatResistance;
}
// check heat resistance against user
var burnedHand = light.CurrentLit && res < lightBulb.BurningTemperature;
if (burnedHand)
{
var damage = _damageableSystem.TryChangeDamage(userUid, light.Damage, origin: userUid);
// If damage is null then the entity could not take heat damage so they did not get burned.
if (damage != null)
{
var burnMsg = Loc.GetString("powered-light-component-burn-hand");
_popupSystem.PopupEntity(burnMsg, uid, userUid);
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} burned their hand on {ToPrettyString(args.Target):target} and received {damage.GetTotal():damage} damage");
_audio.PlayEntity(light.BurnHandSound, Filter.Pvs(uid), uid, true);
args.Handled = true;
return;
}
}
}
//removing a broken/burned bulb, so allow instant removal
if(TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal)
{
@@ -435,6 +400,10 @@ namespace Content.Server.Light.EntitySystems
if (softness != null)
_pointLight.SetSoftness(uid, (float) softness, pointLight);
}
// light bulbs burn your hands!
if (TryComp<DamageOnInteractComponent>(uid, out var damageOnInteractComp))
_damageOnInteractSystem.SetIsDamageActiveTo((uid, damageOnInteractComp), value);
}
public void ToggleLight(EntityUid uid, PoweredLightComponent? light = null)

View File

@@ -35,6 +35,7 @@ public sealed class LightningTargetSystem : EntitySystem
uid.Comp.ExplosionPrototype,
uid.Comp.TotalIntensity, uid.Comp.Dropoff,
uid.Comp.MaxTileIntensity,
uid,
canCreateVacuum: false);
}
}

View File

@@ -85,8 +85,7 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
DistanceThreshold = SharedInteractionSystem.InteractionRange,
BreakOnDamage = true,
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
NeedHand = true,
},
out var doAfterId);
@@ -166,7 +165,6 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
{
BreakOnDamage = true,
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
},
out var doAfterId);
@@ -245,7 +243,6 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
{
DistanceThreshold = SharedInteractionSystem.InteractionRange,
BreakOnDamage = true,
BreakOnHandChange = false,
NeedHand = true
},
out var doAfterId);
@@ -324,8 +321,7 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
{
BreakOnDamage = true,
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
NeedHand = true,
},
out var doAfterId);

View File

@@ -42,7 +42,7 @@ namespace Content.Server.Mapping
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("cmd-savemap-server"));
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking;
using Content.Server.Popups;
@@ -48,7 +48,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing,
before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)});
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged);
}
private void OnStartup(Entity<MaterialReclaimerComponent> entity, ref ComponentStartup args)
@@ -86,12 +86,11 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
args.Handled = TryStartProcessItem(entity.Owner, args.Used, entity.Comp, args.User);
}
private void OnSuicide(Entity<MaterialReclaimerComponent> entity, ref SuicideEvent args)
private void OnSuicideByEnvironment(Entity<MaterialReclaimerComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Bloodloss);
var victim = args.Victim;
if (TryComp(victim, out ActorComponent? actor) &&
_mind.TryGetMind(actor.PlayerSession, out var mindId, out var mind))
@@ -103,12 +102,15 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
}
}
_popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others", ("victim", Identity.Entity(victim, EntityManager))),
_popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others",
("victim", Identity.Entity(victim, EntityManager))),
victim,
Filter.PvsExcept(victim, entityManager: EntityManager), true);
Filter.PvsExcept(victim, entityManager: EntityManager),
true);
_body.GibBody(victim, true);
_appearance.SetData(entity.Owner, RecyclerVisuals.Bloody, true);
args.Handled = true;
}
private void OnActivePowerChanged(Entity<ActiveMaterialReclaimerComponent> entity, ref PowerChangedEvent args)

View File

@@ -106,11 +106,11 @@ namespace Content.Server.Medical.BiomassReclaimer
SubscribeLocalEvent<BiomassReclaimerComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<BiomassReclaimerComponent, ClimbedOnEvent>(OnClimbedOn);
SubscribeLocalEvent<BiomassReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<BiomassReclaimerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<BiomassReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<BiomassReclaimerComponent, ReclaimerDoAfterEvent>(OnDoAfter);
}
private void OnSuicide(Entity<BiomassReclaimerComponent> ent, ref SuicideEvent args)
private void OnSuicideByEnvironment(Entity<BiomassReclaimerComponent> ent, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
@@ -123,7 +123,7 @@ namespace Content.Server.Medical.BiomassReclaimer
_popup.PopupEntity(Loc.GetString("biomass-reclaimer-suicide-others", ("victim", args.Victim)), ent, PopupType.LargeCaution);
StartProcessing(args.Victim, ent);
args.SetHandled(SuicideKind.Blunt);
args.Handled = true;
}
private void OnInit(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentInit args)
@@ -169,7 +169,7 @@ namespace Content.Server.Medical.BiomassReclaimer
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, delay, new ReclaimerDoAfterEvent(), reclaimer, target: args.Target, used: args.Used)
{
NeedHand = true,
BreakOnMove = true
BreakOnMove = true,
});
}

View File

@@ -119,8 +119,6 @@ public sealed class DefibrillatorSystem : EntitySystem
return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(),
uid, target, uid)
{
BlockDuplicate = true,
BreakOnHandChange = true,
NeedHand = true,
BreakOnMove = !component.AllowDoAfterMovement
});

View File

@@ -86,11 +86,14 @@ public sealed class HealthAnalyzerSystem : EntitySystem
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
var doAfterCancelled = !_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
{
NeedHand = true,
BreakOnMove = true
BreakOnMove = true,
});
if (args.Target == args.User || doAfterCancelled)
return;
var msg = Loc.GetString("health-analyzer-popup-scan-target", ("user", Identity.Entity(args.User, EntityManager)));
_popupSystem.PopupEntity(msg, args.Target.Value, args.Target.Value, PopupType.Medium);

View File

@@ -14,37 +14,43 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// Choose a random sensor mode when item is spawned.
/// </summary>
[DataField("randomMode")]
[DataField]
public bool RandomMode = true;
/// <summary>
/// If true user can't change suit sensor mode
/// </summary>
[DataField("controlsLocked")]
[DataField]
public bool ControlsLocked = false;
/// <summary>
/// How much time it takes to change another player's sensors
/// </summary>
[DataField]
public float SensorsTime = 1.75f;
/// <summary>
/// Current sensor mode. Can be switched by user verbs.
/// </summary>
[DataField("mode")]
[DataField]
public SuitSensorMode Mode = SuitSensorMode.SensorOff;
/// <summary>
/// Activate sensor if user wear it in this slot.
/// </summary>
[DataField("activationSlot")]
[DataField]
public string ActivationSlot = "jumpsuit";
/// <summary>
/// Activate sensor if user has this in a sensor-compatible container.
/// </summary>
[DataField("activationContainer")]
[DataField]
public string? ActivationContainer;
/// <summary>
/// How often does sensor update its owners status (in seconds). Limited by the system update rate.
/// </summary>
[DataField("updateRate")]
[DataField]
public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f);
/// <summary>
@@ -56,7 +62,7 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// Next time when sensor updated owners status
/// </summary>
[DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdate = TimeSpan.Zero;

View File

@@ -8,10 +8,13 @@ using Content.Server.GameTicking;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -35,6 +38,9 @@ public sealed class SuitSensorSystem : EntitySystem
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
public override void Initialize()
{
@@ -49,6 +55,7 @@ public sealed class SuitSensorSystem : EntitySystem
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
SubscribeLocalEvent<SuitSensorComponent, SuitSensorChangeDoAfterEvent>(OnSuitSensorDoAfter);
}
public override void Update(float frameTime)
@@ -205,7 +212,14 @@ public sealed class SuitSensorSystem : EntitySystem
return;
// standard interaction checks
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
if (!args.CanInteract || args.Hands == null)
return;
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
return;
// check if target is incapacitated (cuffed, dead, etc)
if (component.User != null && args.User != component.User && _actionBlocker.CanInteract(component.User.Value, null))
return;
args.Verbs.UnionWith(new[]
@@ -239,7 +253,7 @@ public sealed class SuitSensorSystem : EntitySystem
args.Disabled = true;
component.PreviousMode = component.Mode;
SetSensor(uid, SuitSensorMode.SensorOff, null, component);
SetSensor((uid, component), SuitSensorMode.SensorOff, null);
component.PreviousControlsLocked = component.ControlsLocked;
component.ControlsLocked = true;
@@ -247,7 +261,7 @@ public sealed class SuitSensorSystem : EntitySystem
private void OnEmpFinished(EntityUid uid, SuitSensorComponent component, ref EmpDisabledRemoved args)
{
SetSensor(uid, component.PreviousMode, null, component);
SetSensor((uid, component), component.PreviousMode, null);
component.ControlsLocked = component.PreviousControlsLocked;
}
@@ -259,7 +273,7 @@ public sealed class SuitSensorSystem : EntitySystem
Disabled = component.Mode == mode,
Priority = -(int) mode, // sort them in descending order
Category = VerbCategory.SetSensor,
Act = () => SetSensor(uid, mode, userUid, component)
Act = () => TrySetSensor((uid, component), mode, userUid)
};
}
@@ -287,18 +301,46 @@ public sealed class SuitSensorSystem : EntitySystem
return Loc.GetString(name);
}
public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = null,
SuitSensorComponent? component = null)
public void TrySetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid userUid)
{
if (!Resolve(uid, ref component))
var comp = sensors.Comp;
if (!Resolve(sensors, ref comp))
return;
component.Mode = mode;
if (comp.User == null || userUid == comp.User)
SetSensor(sensors, mode, userUid);
else
{
var doAfterEvent = new SuitSensorChangeDoAfterEvent(mode);
var doAfterArgs = new DoAfterArgs(EntityManager, userUid, comp.SensorsTime, doAfterEvent, sensors)
{
BreakOnMove = true,
BreakOnDamage = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
}
}
private void OnSuitSensorDoAfter(Entity<SuitSensorComponent> sensors, ref SuitSensorChangeDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
SetSensor(sensors, args.Mode, args.User);
}
public void SetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid? userUid = null)
{
var comp = sensors.Comp;
comp.Mode = mode;
if (userUid != null)
{
var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
_popupSystem.PopupEntity(msg, uid, userUid.Value);
_popupSystem.PopupEntity(msg, sensors, userUid.Value);
}
}
@@ -323,11 +365,10 @@ public sealed class SuitSensorSystem : EntitySystem
userName = card.Comp.FullName;
if (card.Comp.JobTitle != null)
userJob = card.Comp.JobTitle;
if (card.Comp.JobIcon != null)
userJobIcon = card.Comp.JobIcon;
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString(department));
userJobDepartments.Add(Loc.GetString($"department-{department}"));
}
// get health mob state

View File

@@ -6,6 +6,7 @@ using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
@@ -28,6 +29,7 @@ namespace Content.Server.Medical
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly ThirstSystem _thirst = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
/// <summary>
/// Make an entity vomit, if they have a stomach.
@@ -83,7 +85,7 @@ namespace Content.Server.Medical
}
// Makes a vomit solution the size of 90% of the chemicals removed from the chemstream
solution.AddReagent("Vomit", vomitAmount); // TODO: Dehardcode vomit prototype
solution.AddReagent(new ReagentId("Vomit", _bloodstream.GetEntityBloodData(uid)), vomitAmount); // TODO: Dehardcode vomit prototype
}
if (_puddle.TrySpillAt(uid, solution, out var puddle, false))

View File

@@ -38,7 +38,7 @@ public sealed class CrematoriumSystem : EntitySystem
SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb);
SubscribeLocalEvent<CrematoriumComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<CrematoriumComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen);
}
@@ -146,11 +146,10 @@ public sealed class CrematoriumSystem : EntitySystem
_audio.PlayPvs(component.CremateFinishSound, uid);
}
private void OnSuicide(EntityUid uid, CrematoriumComponent component, SuicideEvent args)
private void OnSuicideByEnvironment(EntityUid uid, CrematoriumComponent component, SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
args.SetHandled(SuicideKind.Heat);
var victim = args.Victim;
if (TryComp(victim, out ActorComponent? actor) && _minds.TryGetMind(victim, out var mindId, out var mind))
@@ -179,6 +178,7 @@ public sealed class CrematoriumSystem : EntitySystem
}
_entityStorage.CloseStorage(uid);
Cremate(uid, component);
args.Handled = true;
}
public override void Update(float frameTime)

View File

@@ -16,6 +16,7 @@ public sealed class NPCCommand : IConsoleCommand
{
if (shell.Player is not { } playerSession)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
@@ -7,6 +8,7 @@ public sealed partial class UnPullOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
private PullingSystem _pulling = default!;
private ActionBlockerSystem _actionBlocker = default!;
private EntityQuery<PullableComponent> _pullableQuery;
@@ -16,6 +18,7 @@ public sealed partial class UnPullOperator : HTNOperator
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_actionBlocker = sysManager.GetEntitySystem<ActionBlockerSystem>();
_pulling = sysManager.GetEntitySystem<PullingSystem>();
_pullableQuery = _entManager.GetEntityQuery<PullableComponent>();
}
@@ -25,7 +28,8 @@ public sealed partial class UnPullOperator : HTNOperator
base.Startup(blackboard);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_pulling.TryStopPull(owner, _pullableQuery.GetComponent(owner), owner);
if (_actionBlocker.CanInteract(owner, owner)) //prevents handcuffed monkeys from pulling etc.
_pulling.TryStopPull(owner, _pullableQuery.GetComponent(owner), owner);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)

View File

@@ -1,4 +1,4 @@
using Content.Server.Buckle.Systems;
using Content.Server.Buckle.Systems;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
@@ -19,7 +19,7 @@ public sealed partial class UnbuckleOperator : HTNOperator
{
base.Startup(blackboard);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_buckle.Unbuckle(owner, null);
_buckle.TryUnbuckle(owner, owner, false);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)

View File

@@ -4,10 +4,10 @@ using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Server.Sticky.Events;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Sticky;
using Robust.Shared.GameObjects;
namespace Content.Server.Ninja.Systems;
@@ -34,7 +34,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
/// <summary>
/// Require that the planter is a ninja and the charge is near the target warp point.
/// </summary>
private void OnAttemptStick(EntityUid uid, SpiderChargeComponent comp, AttemptEntityStickEvent args)
private void OnAttemptStick(EntityUid uid, SpiderChargeComponent comp, ref AttemptEntityStickEvent args)
{
if (args.Cancelled)
return;
@@ -67,7 +67,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
/// <summary>
/// Allows greentext to occur after exploding.
/// </summary>
private void OnStuck(EntityUid uid, SpiderChargeComponent comp, EntityStuckEvent args)
private void OnStuck(EntityUid uid, SpiderChargeComponent comp, ref EntityStuckEvent args)
{
comp.Planter = args.User;
}

View File

@@ -590,7 +590,7 @@ public sealed class NukeSystem : EntitySystem
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
};
if (!_doAfter.TryStartDoAfter(doAfter))

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components;
[RegisterComponent, Access(typeof(FoodSystem))]
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
public sealed partial class FoodComponent : Component
{
[DataField]
@@ -17,7 +17,7 @@ public sealed partial class FoodComponent : Component
public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating");
[DataField]
public EntProtoId? Trash;
public List<EntProtoId> Trash = new();
[DataField]
public FixedPoint2? TransferAmount = FixedPoint2.New(5);

View File

@@ -48,9 +48,12 @@ namespace Content.Server.Nutrition.EntitySystems
{
_puddle.TrySpillAt(uid, solution, out _, false);
}
if (!string.IsNullOrEmpty(foodComp.Trash))
if (foodComp.Trash.Count == 0)
{
EntityManager.SpawnEntity(foodComp.Trash, Transform(uid).Coordinates);
foreach (var trash in foodComp.Trash)
{
EntityManager.SpawnEntity(trash, Transform(uid).Coordinates);
}
}
}
ActivatePayload(uid);

View File

@@ -0,0 +1,140 @@
using System.Text;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
namespace Content.Server.Nutrition.EntitySystems;
public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FoodSequenceStartPointComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInteractUsing(Entity<FoodSequenceStartPointComponent> ent, ref InteractUsingEvent args)
{
if (TryComp<FoodSequenceElementComponent>(args.Used, out var sequenceElement))
TryAddFoodElement(ent, (args.Used, sequenceElement), args.User);
}
private bool TryAddFoodElement(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element, EntityUid? user = null)
{
FoodSequenceElementEntry? elementData = null;
foreach (var entry in element.Comp.Entries)
{
if (entry.Key == start.Comp.Key)
{
elementData = entry.Value;
break;
}
}
if (elementData is null)
return false;
//if we run out of space, we can still put in one last, final finishing element.
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementData.Value.Final || start.Comp.Finished)
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value);
return false;
}
if (elementData.Value.Sprite is not null)
{
start.Comp.FoodLayers.Add(elementData.Value);
Dirty(start);
}
if (elementData.Value.Final)
start.Comp.Finished = true;
UpdateFoodName(start);
MergeFoodSolutions(start, element);
MergeFlavorProfiles(start, element);
MergeTrash(start, element);
QueueDel(element);
return true;
}
private void UpdateFoodName(Entity<FoodSequenceStartPointComponent> start)
{
if (start.Comp.NameGeneration is null)
return;
var content = new StringBuilder();
var separator = "";
if (start.Comp.ContentSeparator is not null)
separator = Loc.GetString(start.Comp.ContentSeparator);
HashSet<LocId> existedContentNames = new();
foreach (var layer in start.Comp.FoodLayers)
{
if (layer.Name is not null && !existedContentNames.Contains(layer.Name.Value))
{
content.Append(Loc.GetString(layer.Name.Value));
existedContentNames.Add(layer.Name.Value);
}
content.Append(separator);
}
var newName = Loc.GetString(start.Comp.NameGeneration.Value,
("prefix", start.Comp.NamePrefix is not null ? Loc.GetString(start.Comp.NamePrefix) : ""),
("content", content),
("suffix", start.Comp.NameSuffix is not null ? Loc.GetString(start.Comp.NameSuffix) : ""));
_metaData.SetEntityName(start, newName);
}
private void MergeFoodSolutions(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element)
{
if (!_solutionContainer.TryGetSolution(start.Owner, start.Comp.Solution, out var startSolutionEntity, out var startSolution))
return;
if (!_solutionContainer.TryGetSolution(element.Owner, element.Comp.Solution, out _, out var elementSolution))
return;
startSolution.MaxVolume += elementSolution.MaxVolume;
_solutionContainer.TryAddSolution(startSolutionEntity.Value, elementSolution);
}
private void MergeFlavorProfiles(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element)
{
if (!TryComp<FlavorProfileComponent>(start, out var startProfile))
return;
if (!TryComp<FlavorProfileComponent>(element, out var elementProfile))
return;
foreach (var flavor in elementProfile.Flavors)
{
if (startProfile != null && !startProfile.Flavors.Contains(flavor))
startProfile.Flavors.Add(flavor);
}
}
private void MergeTrash(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element)
{
if (!TryComp<FoodComponent>(start, out var startFood))
return;
if (!TryComp<FoodComponent>(element, out var elementFood))
return;
foreach (var trash in elementFood.Trash)
{
startFood.Trash.Add(trash);
}
}
}

View File

@@ -335,27 +335,31 @@ public sealed class FoodSystem : EntitySystem
if (ev.Cancelled)
return;
if (string.IsNullOrEmpty(component.Trash))
if (component.Trash.Count == 0)
{
QueueDel(food);
return;
}
//We're empty. Become trash.
//cache some data as we remove food, before spawning trash and passing it to the hand.
var position = _transform.GetMapCoordinates(food);
var finisher = Spawn(component.Trash, position);
var trashes = component.Trash;
var tryPickup = _hands.IsHolding(user, food, out _);
// If the user is holding the item
if (_hands.IsHolding(user, food, out var hand))
Del(food);
foreach (var trash in trashes)
{
Del(food);
var spawnedTrash = Spawn(trash, position);
// Put the trash in the user's hand
_hands.TryPickup(user, finisher, hand);
return;
// If the user is holding the item
if (tryPickup)
{
// Put the trash in the user's hand
_hands.TryPickupAnyHand(user, spawnedTrash);
}
}
QueueDel(food);
}
private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)

View File

@@ -125,18 +125,21 @@ namespace Content.Server.Nutrition.EntitySystems
if (ev.Cancelled)
return;
if (string.IsNullOrEmpty(foodComp.Trash))
if (foodComp.Trash.Count == 0)
{
QueueDel(uid);
return;
}
// Locate the sliced food and spawn its trash
var trashUid = Spawn(foodComp.Trash, _xformSystem.GetMapCoordinates(uid));
foreach (var trash in foodComp.Trash)
{
var trashUid = Spawn(trash, _xformSystem.GetMapCoordinates(uid));
// try putting the trash in the food's container too, to be consistent with slice spawning?
_xformSystem.DropNextTo(trashUid, uid);
_xformSystem.SetLocalRotation(trashUid, 0);
// try putting the trash in the food's container too, to be consistent with slice spawning?
_xformSystem.DropNextTo(trashUid, uid);
_xformSystem.SetLocalRotation(trashUid, 0);
}
QueueDel(uid);
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Popups;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
@@ -25,7 +26,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
base.Initialize();
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem) });
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
}
/// <summary>

View File

@@ -0,0 +1,23 @@
using Content.Server.Objectives.Systems;
using Content.Server.Thief.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// An abstract component that allows other systems to count adjacent objects as "stolen" when controlling other systems
/// </summary>
[RegisterComponent, Access(typeof(StealConditionSystem), typeof(ThiefBeaconSystem))]
public sealed partial class StealAreaComponent : Component
{
[DataField]
public bool Enabled = true;
[DataField]
public float Range = 1f;
/// <summary>
/// all the minds that will be credited with stealing from this area.
/// </summary>
[DataField]
public HashSet<EntityUid> Owners = new();
}

View File

@@ -22,11 +22,18 @@ public sealed partial class StealConditionComponent : Component
[DataField]
public bool VerifyMapExistence = true;
/// <summary>
/// If true, counts objects that are close to steal areas.
/// </summary>
[DataField]
public bool CheckStealAreas = false;
/// <summary>
/// If the target may be alive but has died, it will not be counted
/// </summary>
[DataField]
public bool CheckAlive = false;
/// <summary>
/// The minimum number of items you need to steal to fulfill a objective
/// </summary>

View File

@@ -21,16 +21,15 @@ public sealed class StealConditionSystem : EntitySystem
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
private EntityQuery<ContainerManagerComponent> _containerQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
public override void Initialize()
{
base.Initialize();
_containerQuery = GetEntityQuery<ContainerManagerComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
SubscribeLocalEvent<StealConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<StealConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
@@ -96,25 +95,33 @@ public sealed class StealConditionSystem : EntitySystem
if (!_containerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager))
return 0;
var stack = new Stack<ContainerManagerComponent>();
var containerStack = new Stack<ContainerManagerComponent>();
var count = 0;
//check stealAreas
if (condition.CheckStealAreas)
{
var areasQuery = AllEntityQuery<StealAreaComponent>();
while (areasQuery.MoveNext(out var uid, out var area))
{
if (!area.Owners.Contains(mind.Owner))
continue;
var nearestEnt = _lookup.GetEntitiesInRange(uid, area.Range);
foreach (var ent in nearestEnt)
{
CheckEntity(ent, condition, ref containerStack, ref count);
}
}
}
//check pulling object
if (TryComp<PullerComponent>(mind.OwnedEntity, out var pull)) //TO DO: to make the code prettier? don't like the repetition
{
var pulledEntity = pull.Pulling;
if (pulledEntity != null)
{
// check if this is the item
count += CheckStealTarget(pulledEntity.Value, condition);
//we don't check the inventories of sentient entity
if (!HasComp<MindContainerComponent>(pulledEntity))
{
// if it is a container check its contents
if (_containerQuery.TryGetComponent(pulledEntity, out var containerManager))
stack.Push(containerManager);
}
CheckEntity(pulledEntity.Value, condition, ref containerStack, ref count);
}
}
@@ -131,16 +138,30 @@ public sealed class StealConditionSystem : EntitySystem
// if it is a container check its contents
if (_containerQuery.TryGetComponent(entity, out var containerManager))
stack.Push(containerManager);
containerStack.Push(containerManager);
}
}
} while (stack.TryPop(out currentManager));
} while (containerStack.TryPop(out currentManager));
var result = count / (float) condition.CollectionSize;
result = Math.Clamp(result, 0, 1);
return result;
}
private void CheckEntity(EntityUid entity, StealConditionComponent condition, ref Stack<ContainerManagerComponent> containerStack, ref int counter)
{
// check if this is the item
counter += CheckStealTarget(entity, condition);
//we don't check the inventories of sentient entity
if (!TryComp<MindContainerComponent>(entity, out var pullMind))
{
// if it is a container check its contents
if (_containerQuery.TryGetComponent(entity, out var containerManager))
containerStack.Push(containerManager);
}
}
private int CheckStealTarget(EntityUid entity, StealConditionComponent condition)
{
// check if this is the target

View File

@@ -1,11 +1,5 @@
namespace Content.Server.Pinpointer;
[RegisterComponent]
public sealed partial class StationMapComponent : Component
{
}
/// <summary>
/// Added to an entity using station map so when its parent changes we reset it.
/// </summary>

View File

@@ -73,8 +73,10 @@ public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.StartTime, new GeneratorStartedEvent(), uid, uid)
{
BreakOnDamage = true, BreakOnMove = true, RequireCanInteract = true,
NeedHand = true
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
BreakOnDropItem = false,
});
}

View File

@@ -54,7 +54,7 @@ public sealed class ResistLockerSystem : EntitySystem
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = false //No hands 'cause we be kickin'
NeedHand = false, //No hands 'cause we be kickin'
};
resistLockerComponent.IsResisting = true;

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