Merge branch 'master' of https://github.com/space-wizards/space-station-14 into map-load-refactor

This commit is contained in:
ElectroJr
2025-02-16 16:52:51 +13:00
1624 changed files with 385006 additions and 263837 deletions

View File

@@ -1,7 +1,6 @@
using Content.Server.Wires;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Emag.Components;
using Content.Shared.Wires;
namespace Content.Server.Access;
@@ -31,11 +30,9 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
if (!EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
{
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
}
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
return true;
}
@@ -58,7 +55,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
{
if (!wire.IsCut)
{
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access) && !EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
{
access.Enabled = true;
EntityManager.Dirty(wire.Owner, access);

View File

@@ -245,6 +245,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
Dirty(accessReaderEnt.Value);
}

View File

@@ -118,8 +118,9 @@ namespace Content.Server.Administration.Commands
}
var xform = _entManager.GetComponent<TransformComponent>(playerEntity);
var xformSystem = _entManager.System<SharedTransformSystem>();
xform.Coordinates = coords;
xform.AttachToGridOrMap();
xformSystem.AttachToGridOrMap(playerEntity, xform);
if (_entManager.TryGetComponent(playerEntity, out PhysicsComponent? physics))
{
_entManager.System<SharedPhysicsSystem>().SetLinearVelocity(playerEntity, Vector2.Zero, body: physics);

View File

@@ -1,6 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
namespace Content.Server.Administration.Managers;
@@ -30,36 +28,15 @@ public sealed partial class BanManager
private TimeSpan _banNotificationRateLimitStart;
private int _banNotificationRateLimitCount;
private void OnDatabaseNotification(DatabaseNotification notification)
private bool OnDatabaseNotificationEarlyFilter()
{
if (notification.Channel != BanNotificationChannel)
return;
if (notification.Payload == null)
{
_sawmill.Error("Got ban notification with null payload!");
return;
}
BanNotificationData data;
try
{
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
return;
}
if (!CheckBanRateLimit())
{
_sawmill.Verbose("Not processing ban notification due to rate limit");
return;
return false;
}
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
return true;
}
private async void ProcessBanNotification(BanNotificationData data)

View File

@@ -53,7 +53,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
{
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToNotifications(OnDatabaseNotification);
_db.SubscribeToJsonNotification<BanNotificationData>(
_taskManager,
_sawmill,
BanNotificationChannel,
ProcessBanNotification,
OnDatabaseNotificationEarlyFilter);
_userDbData.AddOnLoadPlayer(CachePlayerData);
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
@@ -160,6 +165,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
null);
await _db.AddServerBanAsync(banDef);
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher!
var adminName = banningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");

View File

@@ -0,0 +1,114 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration.Managers;
/// <summary>
/// Handles kicking people that connect to multiple servers on the same DB at once.
/// </summary>
/// <seealso cref="CCVars.AdminAllowMultiServerPlay"/>
public sealed class MultiServerKickManager
{
public const string NotificationChannel = "multi_server_kick";
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IServerDbManager _dbManager = null!;
[Dependency] private readonly ILogManager _logManager = null!;
[Dependency] private readonly IConfigurationManager _cfg = null!;
[Dependency] private readonly IAdminManager _adminManager = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IServerNetManager _netManager = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = null!;
private ISawmill _sawmill = null!;
private bool _allowed;
public void Initialize()
{
_sawmill = _logManager.GetSawmill("multi_server_kick");
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_cfg.OnValueChanged(CCVars.AdminAllowMultiServerPlay, b => _allowed = b, true);
_dbManager.SubscribeToJsonNotification<NotificationData>(
_taskManager,
_sawmill,
NotificationChannel,
OnNotification,
OnNotificationEarlyFilter
);
}
// ReSharper disable once AsyncVoidMethod
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (_allowed)
return;
if (e.NewStatus != SessionStatus.InGame)
return;
// Send notification to other servers so they can kick this player that just connected.
try
{
await _dbManager.SendNotification(new DatabaseNotification
{
Channel = NotificationChannel,
Payload = JsonSerializer.Serialize(new NotificationData
{
PlayerId = e.Session.UserId,
ServerId = (await _serverDbEntry.ServerEntity).Id,
}),
});
}
catch (Exception ex)
{
_sawmill.Error($"Failed to send notification for multi server kick: {ex}");
}
}
private bool OnNotificationEarlyFilter()
{
if (_allowed)
{
_sawmill.Verbose("Received notification for player join, but multi server play is allowed on this server. Ignoring");
return false;
}
return true;
}
// ReSharper disable once AsyncVoidMethod
private async void OnNotification(NotificationData notification)
{
if (!_playerManager.TryGetSessionById(new NetUserId(notification.PlayerId), out var player))
return;
if (notification.ServerId == (await _serverDbEntry.ServerEntity).Id)
return;
if (_adminManager.IsAdmin(player, includeDeAdmin: true))
return;
_sawmill.Info($"Kicking {player} for connecting to another server. Multi-server play is not allowed.");
_netManager.DisconnectChannel(player.Channel, _loc.GetString("multi-server-kick-reason"));
}
private sealed class NotificationData
{
[JsonPropertyName("player_id")]
public Guid PlayerId { get; set; }
[JsonPropertyName("server_id")]
public int ServerId { get; set; }
}
}

View File

@@ -11,6 +11,7 @@ using Content.Server.StationRecords.Systems;
using Content.Shared.Administration;
using Content.Shared.Administration.Events;
using Content.Shared.CCVar;
using Content.Shared.Forensics.Components;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
@@ -82,13 +83,14 @@ public sealed class AdminSystem : EntitySystem
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
SubscribeLocalEvent<ActorComponent, EntityRenamedEvent>(OnPlayerRenamed);
SubscribeLocalEvent<ActorComponent, IdentityChangedEvent>(OnIdentityChanged);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
@@ -144,12 +146,9 @@ public sealed class AdminSystem : EntitySystem
return value ?? null;
}
private void OnIdentityChanged(ref IdentityChangedEvent ev)
private void OnIdentityChanged(Entity<ActorComponent> ent, ref IdentityChangedEvent ev)
{
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
return;
UpdatePlayerList(actor.PlayerSession);
UpdatePlayerList(ent.Comp.PlayerSession);
}
private void OnRoleEvent(RoleEvent ev)

View File

@@ -137,7 +137,7 @@ public sealed partial class AdminVerbSystem
var board = Spawn("ChessBoard", xform.Coordinates);
var session = _tabletopSystem.EnsureSession(Comp<TabletopGameComponent>(board));
xform.Coordinates = EntityCoordinates.FromMap(_mapManager, session.Position);
xform.WorldRotation = Angle.Zero;
_transformSystem.SetWorldRotationNoLerp((args.Target, xform), Angle.Zero);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))
@@ -892,5 +892,36 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
};
args.Verbs.Add(superslip);
var omniaccentName = Loc.GetString("admin-smite-omni-accent-name").ToLowerInvariant();
Verb omniaccent = new()
{
Text = omniaccentName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Interface/Actions/voice-mask.rsi"), "icon"),
Act = () =>
{
EnsureComp<BarkAccentComponent>(args.Target);
EnsureComp<BleatingAccentComponent>(args.Target);
EnsureComp<FrenchAccentComponent>(args.Target);
EnsureComp<GermanAccentComponent>(args.Target);
EnsureComp<LizardAccentComponent>(args.Target);
EnsureComp<MobsterAccentComponent>(args.Target);
EnsureComp<MothAccentComponent>(args.Target);
EnsureComp<OwOAccentComponent>(args.Target);
EnsureComp<SkeletonAccentComponent>(args.Target);
EnsureComp<SouthernAccentComponent>(args.Target);
EnsureComp<SpanishAccentComponent>(args.Target);
EnsureComp<StutteringAccentComponent>(args.Target);
if (_random.Next(0, 8) == 0)
{
EnsureComp<BackwardsAccentComponent>(args.Target); // was asked to make this at a low chance idk
}
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
};
args.Verbs.Add(omniaccent);
}
}

View File

@@ -370,7 +370,7 @@ namespace Content.Server.Administration.Systems
}
if (lawBoundComponent != null && target != null)
if (lawBoundComponent != null && target != null && _adminManager.HasAdminFlag(player, AdminFlags.Moderator))
{
args.Verbs.Add(new Verb()
{

View File

@@ -1,38 +1,79 @@
using Content.Server.Administration;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
namespace Content.Server.Announcements
namespace Content.Server.Announcements;
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceCommand : LocalizedEntityCommands
{
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceCommand : IConsoleCommand
{
public string Command => "announce";
public string Description => "Send an in-game announcement.";
public string Help => $"{Command} <sender> <message> or {Command} <message> to send announcement as CentCom.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var chat = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _res = default!;
if (args.Length == 0)
public override string Command => "announce";
public override string Description => Loc.GetString("cmd-announce-desc");
public override string Help => Loc.GetString("cmd-announce-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
return;
case > 4:
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
var message = args[0];
var sender = Loc.GetString("cmd-announce-sender");
var color = Color.Gold;
var sound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
// Optional sender argument
if (args.Length >= 2)
sender = args[1];
// Optional color argument
if (args.Length >= 3)
{
try
{
shell.WriteError("Not enough arguments! Need at least 1.");
color = Color.FromHex(args[2]);
}
catch
{
shell.WriteError(Loc.GetString("shell-invalid-color-hex"));
return;
}
if (args.Length == 1)
{
chat.DispatchGlobalAnnouncement(args[0], colorOverride: Color.Gold);
}
else
{
// Explicit IEnumerable<string> due to overload ambiguity on .NET 9
var message = string.Join(' ', (IEnumerable<string>)new ArraySegment<string>(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");
}
// Optional sound argument
if (args.Length >= 4)
sound = new SoundPathSpecifier(args[3]);
_chat.DispatchGlobalAnnouncement(message, sender, true, sound, color);
shell.WriteLine(Loc.GetString("shell-command-success"));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-message")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-sender")),
3 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-color")),
4 => CompletionResult.FromHintOptions(
CompletionHelper.AudioFilePath(args[3], _proto, _res),
Loc.GetString("cmd-announce-arg-sound")
),
_ => CompletionResult.Empty
};
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Jittering;
using Content.Server.Mind;
using Content.Server.Stunnable;
using Content.Shared.Actions;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
@@ -24,8 +23,6 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly IChatManager _chat = default!;

View File

@@ -116,8 +116,11 @@ public sealed class TechAnomalySystem : EntitySystem
if (_random.Prob(tech.Comp.EmagSupercritProbability))
{
_emag.DoEmagEffect(tech, source);
_emag.DoEmagEffect(tech, sink);
var sourceEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
RaiseLocalEvent(source, ref sourceEv);
var sinkEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
RaiseLocalEvent(sink, ref sinkEv);
}
CreateNewLink(tech, source, sink);

View File

@@ -10,10 +10,8 @@ using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -25,11 +23,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
private const float UpdateTime = 1.0f;
@@ -54,7 +50,7 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
}
#region Event handling
#region Event handling
private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args)
{

View File

@@ -61,9 +61,9 @@ public sealed partial class AtmosphereSystem
mixtures[5].Temperature = 5000f;
// 6: (Walk-In) Freezer
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
mixtures[6].Temperature = 235f; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
// 7: Nitrogen (101kpa) for vox rooms
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);

View File

@@ -118,7 +118,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
// Used by ExperiencePressureDifference to correct push/throw directions from tile-relative to physics world.
var gridWorldRotation = xforms.GetComponent(gridAtmosphere).WorldRotation;
var gridWorldRotation = _transformSystem.GetWorldRotation(gridAtmosphere);
// If we're using monstermos, smooth out the yeet direction to follow the flow
if (MonstermosEqualization)

View File

@@ -178,7 +178,7 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.Volume = tile.Air.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Temperature = tile.Air.Temperature;
}
else
@@ -187,7 +187,7 @@ namespace Content.Server.Atmos.EntitySystems
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;
tile.Hotspot.Volume = affected.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
Merge(tile.Air, affected);
}

View File

@@ -9,10 +9,12 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Database;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Systems;
@@ -37,6 +39,7 @@ namespace Content.Server.Atmos.Monitor.Systems;
public sealed class AirAlarmSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
@@ -296,6 +299,7 @@ public sealed class AirAlarmSystem : EntitySystem
addr = netConn.Address;
}
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {ToPrettyString(uid)} mode to {args.Mode}");
SetMode(uid, addr, args.Mode, false);
}
else
@@ -307,15 +311,26 @@ public sealed class AirAlarmSystem : EntitySystem
private void OnUpdateAutoMode(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAutoModeMessage args)
{
component.AutoMode = args.Enabled;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {ToPrettyString(uid)} auto mode to {args.Enabled}");
UpdateUI(uid, component);
}
private void OnUpdateThreshold(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmThresholdMessage args)
{
if (AccessCheck(uid, args.Actor, component))
{
if (args.Gas != null)
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} {args.Gas} {args.Type} threshold using {ToPrettyString(uid)}");
else
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} {args.Type} threshold using {ToPrettyString(uid)}");
SetThreshold(uid, args.Address, args.Type, args.Threshold, args.Gas);
}
else
{
UpdateUI(uid, component);
}
}
private void OnUpdateDeviceData(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateDeviceDataMessage args)
@@ -323,6 +338,8 @@ public sealed class AirAlarmSystem : EntitySystem
if (AccessCheck(uid, args.Actor, component)
&& _deviceList.ExistsInDeviceList(uid, args.Address))
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} settings using {ToPrettyString(uid)}");
SetDeviceData(uid, args.Address, args.Data);
}
else
@@ -344,6 +361,7 @@ public sealed class AirAlarmSystem : EntitySystem
case GasVentPumpData ventData:
foreach (string addr in component.VentData.Keys)
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} copied settings to vent {addr}");
SetData(uid, addr, args.Data);
}
break;
@@ -351,6 +369,7 @@ public sealed class AirAlarmSystem : EntitySystem
case GasVentScrubberData scrubberData:
foreach (string addr in component.ScrubberData.Keys)
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} copied settings to scrubber {addr}");
SetData(uid, addr, args.Data);
}
break;
@@ -379,6 +398,7 @@ public sealed class AirAlarmSystem : EntitySystem
if (!_access.IsAllowed(user.Value, uid, reader))
{
_popup.PopupEntity(Loc.GetString("air-alarm-ui-access-denied"), user.Value, user.Value);
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Low, $"{ToPrettyString(user)} attempted to access {ToPrettyString(uid)} without access");
return false;
}

View File

@@ -9,9 +9,11 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Power;
using Content.Shared.Tag;
@@ -25,6 +27,7 @@ namespace Content.Server.Atmos.Monitor.Systems;
// a danger), and atmos (which triggers based on set thresholds).
public sealed class AtmosMonitorSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
@@ -393,21 +396,74 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!Resolve(uid, ref monitor))
return;
// Used for logging after the switch statement
string logPrefix = "";
string logValueSuffix = "";
AtmosAlarmThreshold? logPreviousThreshold = null;
switch (type)
{
case AtmosMonitorThresholdType.Pressure:
logPrefix = "pressure";
logValueSuffix = "kPa";
logPreviousThreshold = monitor.PressureThreshold;
monitor.PressureThreshold = threshold;
break;
case AtmosMonitorThresholdType.Temperature:
logPrefix = "temperature";
logValueSuffix = "K";
logPreviousThreshold = monitor.TemperatureThreshold;
monitor.TemperatureThreshold = threshold;
break;
case AtmosMonitorThresholdType.Gas:
if (gas == null || monitor.GasThresholds == null)
return;
logPrefix = ((Gas) gas).ToString();
logValueSuffix = "kPa";
monitor.GasThresholds.TryGetValue((Gas) gas, out logPreviousThreshold);
monitor.GasThresholds[(Gas) gas] = threshold;
break;
}
// Admin log each change separately rather than logging the whole state
if (logPreviousThreshold != null)
{
if (threshold.Ignore != logPreviousThreshold.Ignore)
{
string enabled = threshold.Ignore ? "disabled" : "enabled";
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} thresholds {enabled}"
);
}
foreach (var change in threshold.GetChanges(logPreviousThreshold))
{
if (change.Current.Enabled != change.Previous?.Enabled)
{
string enabled = change.Current.Enabled ? "enabled" : "disabled";
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} {change.Type} {enabled}"
);
}
if (change.Current.Value != change.Previous?.Value)
{
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} {change.Type} changed from {change.Previous?.Value} {logValueSuffix} to {change.Current.Value} {logValueSuffix}"
);
}
}
}
}
/// <summary>

View File

@@ -21,6 +21,7 @@ public sealed class FireAlarmSystem : EntitySystem
{
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
@@ -77,11 +78,18 @@ public sealed class FireAlarmSystem : EntitySystem
private void OnEmagged(EntityUid uid, FireAlarmComponent component, ref GotEmaggedEvent args)
{
if (TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
{
// Remove the atmos alarmable component permanently from this device.
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
RemCompDeferred<AtmosAlarmableComponent>(uid);
}
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (!TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
return;
// Remove the atmos alarmable component permanently from this device.
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
RemCompDeferred<AtmosAlarmableComponent>(uid);
args.Handled = true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Binary.Components
{
@@ -38,6 +39,7 @@ namespace Content.Server.Atmos.Piping.Binary.Components
public float LowerThreshold { get; set; } = 0.01f;
[DataField("higherThreshold")]
[GuidebookData]
public float HigherThreshold { get; set; } = DefaultHigherThreshold;
public static readonly float DefaultHigherThreshold = 2 * Atmospherics.MaxOutputPressure;

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Trinary.Components
{
@@ -27,6 +28,7 @@ namespace Content.Server.Atmos.Piping.Trinary.Components
[ViewVariables(VVAccess.ReadWrite)]
[DataField("threshold")]
[GuidebookData]
public float Threshold { get; set; } = Atmospherics.OneAtmosphere;
[DataField("maxTransferRate")]

View File

@@ -1,5 +1,6 @@
using Content.Shared.Atmos;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Guidebook;
using Robust.Shared.Audio;
namespace Content.Server.Atmos.Piping.Unary.Components
@@ -61,5 +62,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField("accessDeniedSound")]
public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
#region GuidebookData
[GuidebookData]
public float Volume => Air.Volume;
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Unary.Components
{
@@ -29,6 +30,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
public float MaxTransferRate = Atmospherics.MaxTransferRate;
[DataField("maxPressure")]
[GuidebookData]
public float MaxPressure { get; set; } = GasVolumePumpComponent.DefaultHigherThreshold;
[DataField("inlet")]

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Unary.Components
{
@@ -13,6 +14,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// thermomachine to heat or cool air.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float HeatCapacity = 5000;
[DataField, ViewVariables(VVAccess.ReadWrite)]
@@ -21,6 +23,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// <summary>
/// Tolerance for temperature setpoint hysteresis.
/// </summary>
[GuidebookData]
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float TemperatureTolerance = 2f;
@@ -44,6 +47,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Ignored if heater.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MinTemperature = 73.15f;
/// <summary>
@@ -51,6 +55,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Ignored if freezer.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxTemperature = 593.15f;
/// <summary>
@@ -63,6 +68,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// An percentage of the energy change that is leaked into the surrounding environment rather than the inlet pipe.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float EnergyLeakPercentage;
/// <summary>

View File

@@ -1,6 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.Guidebook;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Piping.Unary.Components
@@ -10,8 +11,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[RegisterComponent]
public sealed partial class GasVentPumpComponent : Component
{
/// <summary>
/// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered.
/// By default, all air vents start enabled, whether linked to an alarm or not.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled { get; set; } = false;
public bool Enabled { get; set; } = true;
[ViewVariables]
public bool IsDirty { get; set; } = false;
@@ -35,6 +40,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// In releasing mode, do not pump when environment pressure is below this limit.
/// </summary>
[DataField]
[GuidebookData]
public float UnderPressureLockoutThreshold = 80; // this must be tuned in conjunction with atmos.mmos_spacing_speed
/// <summary>
@@ -58,7 +64,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField]
public bool IsPressureLockoutManuallyDisabled = false;
/// <summary>
/// The time when the manual pressure lockout will be reenabled.
/// The time when the manual pressure lockout will be reenabled.
/// </summary>
[DataField]
[AutoPausedField]
@@ -101,6 +107,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Max pressure of the target gas (NOT relative to source).
/// </summary>
[DataField]
[GuidebookData]
public float MaxPressure = Atmospherics.MaxOutputPressure;
/// <summary>
@@ -172,5 +179,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
InternalPressureBound = data.InternalPressureBound;
PressureLockoutOverride = data.PressureLockoutOverride;
}
#region GuidebookData
[GuidebookData]
public float DefaultExternalBound => Atmospherics.OneAtmosphere;
#endregion
}
}

View File

@@ -9,8 +9,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[Access(typeof(GasVentScrubberSystem))]
public sealed partial class GasVentScrubberComponent : Component
{
/// <summary>
/// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered.
/// By default, all air scrubbers start enabled, whether linked to an alarm or not.
/// </summary>
[DataField]
public bool Enabled { get; set; } = false;
public bool Enabled { get; set; } = true;
[DataField]
public bool IsDirty { get; set; } = false;

View File

@@ -109,7 +109,15 @@ public sealed class GasCanisterSystem : EntitySystem
var item = canister.GasTankSlot.Item;
_slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
if (canister.ReleaseValve)
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
}
else
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
}
}
private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
@@ -124,24 +132,24 @@ public sealed class GasCanisterSystem : EntitySystem
private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
{
var impact = LogImpact.High;
// filling a jetpack with plasma is less important than filling a room with it
impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High;
var hasItem = canister.GasTankSlot.HasItem;
var impact = hasItem ? LogImpact.Medium : LogImpact.High;
var containedGasDict = new Dictionary<Gas, float>();
var containedGasArray = Enum.GetValues(typeof(Gas));
for (int i = 0; i < containedGasArray.Length; i++)
{
containedGasDict.Add((Gas)i, canister.Air[i]);
}
_adminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");
_adminLogger.Add(
LogType.CanisterValve,
impact,
$"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
canister.ReleaseValve = args.Valve;
DirtyUI(uid, canister);
}
private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
{
return string.Join(", ", canister.Comp.Air);
}
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
{
_atmos.React(canister.Air, canister);

View File

@@ -9,6 +9,9 @@ using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
@@ -16,6 +19,7 @@ using Content.Shared.Atmos.Piping.Unary;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Audio;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
@@ -30,6 +34,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[UsedImplicitly]
public sealed class GasVentPumpSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
@@ -40,6 +45,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
public override void Initialize()
{
base.Initialize();
@@ -63,9 +69,10 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
//Bingo waz here
if (_weldable.IsWelded(uid))
{
return;
}
if (!_powerReceiverSystem.IsPowered(uid))
return;
var nodeName = vent.PumpDirection switch
{
@@ -207,7 +214,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnPowerChanged(EntityUid uid, GasVentPumpComponent component, ref PowerChangedEvent args)
{
component.Enabled = args.Powered;
UpdateState(uid, component);
}
@@ -232,6 +238,44 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (!args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out GasVentPumpData? setData))
break;
var previous = component.ToAirAlarmData();
if (previous.Enabled != setData.Enabled)
{
string enabled = setData.Enabled ? "enabled" : "disabled" ;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} {enabled}");
}
if (previous.PumpDirection != setData.PumpDirection)
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} direction changed to {setData.PumpDirection}");
if (previous.PressureChecks != setData.PressureChecks)
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} pressure check changed to {setData.PressureChecks}");
if (previous.ExternalPressureBound != setData.ExternalPressureBound)
{
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} external pressure bound changed from {previous.ExternalPressureBound} kPa to {setData.ExternalPressureBound} kPa"
);
}
if (previous.InternalPressureBound != setData.InternalPressureBound)
{
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} internal pressure bound changed from {previous.InternalPressureBound} kPa to {setData.InternalPressureBound} kPa"
);
}
if (previous.PressureLockoutOverride != setData.PressureLockoutOverride)
{
string enabled = setData.PressureLockoutOverride ? "enabled" : "disabled" ;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} pressure lockout override {enabled}");
}
component.FromAirAlarmData(setData);
UpdateState(uid, component);
@@ -277,7 +321,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
_ambientSoundSystem.SetAmbience(uid, false);
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Welded, appearance);
}
else if (!vent.Enabled)
else if (!_powerReceiverSystem.IsPowered(uid) || !vent.Enabled)
{
_ambientSoundSystem.SetAmbience(uid, false);
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Off, appearance);

View File

@@ -10,12 +10,15 @@ using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Visuals;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Audio;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Power;
using Content.Shared.Tools.Systems;
@@ -27,6 +30,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[UsedImplicitly]
public sealed class GasVentScrubberSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
@@ -34,6 +38,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly WeldableSystem _weldable = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
public override void Initialize()
{
@@ -55,6 +60,9 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var timeDelta = args.dt;
if (!_powerReceiverSystem.IsPowered(uid))
return;
if (!scrubber.Enabled || !_nodeContainer.TryGetNode(uid, scrubber.OutletName, out PipeNode? outlet))
return;
@@ -138,7 +146,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnPowerChanged(EntityUid uid, GasVentScrubberComponent component, ref PowerChangedEvent args)
{
component.Enabled = args.Powered;
UpdateState(uid, component);
}
@@ -163,6 +170,43 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (!args.Data.TryGetValue(DeviceNetworkConstants.CmdSetState, out GasVentScrubberData? setData))
break;
var previous = component.ToAirAlarmData();
if (previous.Enabled != setData.Enabled)
{
string enabled = setData.Enabled ? "enabled" : "disabled" ;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} {enabled}");
}
// TODO: IgnoreAlarms?
if (previous.PumpDirection != setData.PumpDirection)
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} direction changed to {setData.PumpDirection}");
// TODO: This is iterating through both sets, it could probably be faster but they're both really small sets anyways
foreach (Gas gas in previous.FilterGases)
if (!setData.FilterGases.Contains(gas))
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} {gas} filtering disabled");
foreach (Gas gas in setData.FilterGases)
if (!previous.FilterGases.Contains(gas))
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} {gas} filtering enabled");
if (previous.VolumeRate != setData.VolumeRate)
{
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} volume rate changed from {previous.VolumeRate} L to {setData.VolumeRate} L"
);
}
if (previous.WideNet != setData.WideNet)
{
string enabled = setData.WideNet ? "enabled" : "disabled" ;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(uid)} WideNet {enabled}");
}
component.FromAirAlarmData(setData);
UpdateState(uid, component);
@@ -185,7 +229,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
_ambientSoundSystem.SetAmbience(uid, false);
_appearance.SetData(uid, ScrubberVisuals.State, ScrubberState.Welded, appearance);
}
else if (!scrubber.Enabled)
else if (!_powerReceiverSystem.IsPowered(uid) || !scrubber.Enabled)
{
_ambientSoundSystem.SetAmbience(uid, false);
_appearance.SetData(uid, ScrubberVisuals.State, ScrubberState.Off, appearance);

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Portable
{
@@ -45,5 +46,12 @@ namespace Content.Server.Atmos.Portable
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float TransferRate = 800;
#region GuidebookData
[GuidebookData]
public float Volume => Air.Volume;
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Portable.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Portable;
@@ -23,12 +24,14 @@ public sealed partial class SpaceHeaterComponent : Component
/// Maximum target temperature the device can be set to
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxTemperature = Atmospherics.T20C + 20;
/// <summary>
/// Minimal target temperature the device can be set to
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MinTemperature = Atmospherics.T0C - 10;
/// <summary>

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Atmos.Reactions
var oldHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
var temperature = mixture.Temperature;
var location = holder as TileAtmosphere;
mixture.ReactionResults[GasReaction.Fire] = 0;
mixture.ReactionResults[(byte)GasReaction.Fire] = 0;
// More plasma released at higher temperatures.
var temperatureScale = 0f;
@@ -60,7 +60,7 @@ namespace Content.Server.Atmos.Reactions
energyReleased += Atmospherics.FirePlasmaEnergyReleased * plasmaBurnRate;
energyReleased /= heatScale; // adjust energy to make sure speedup doesn't cause mega temperature rise
mixture.ReactionResults[GasReaction.Fire] += plasmaBurnRate * (1 + oxygenBurnRate);
mixture.ReactionResults[(byte)GasReaction.Fire] += plasmaBurnRate * (1 + oxygenBurnRate);
}
}
@@ -80,7 +80,7 @@ namespace Content.Server.Atmos.Reactions
}
}
return mixture.ReactionResults[GasReaction.Fire] != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction;
return mixture.ReactionResults[(byte)GasReaction.Fire] != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction;
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Atmos.Reactions
var oldHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
var temperature = mixture.Temperature;
var location = holder as TileAtmosphere;
mixture.ReactionResults[GasReaction.Fire] = 0f;
mixture.ReactionResults[(byte)GasReaction.Fire] = 0f;
var burnedFuel = 0f;
var initialTrit = mixture.GetMoles(Gas.Tritium);
@@ -45,7 +45,7 @@ namespace Content.Server.Atmos.Reactions
// Conservation of mass is important.
mixture.AdjustMoles(Gas.WaterVapor, burnedFuel);
mixture.ReactionResults[GasReaction.Fire] += burnedFuel;
mixture.ReactionResults[(byte)GasReaction.Fire] += burnedFuel;
}
energyReleased /= heatScale; // adjust energy to make sure speedup doesn't cause mega temperature rise
@@ -65,7 +65,7 @@ namespace Content.Server.Atmos.Reactions
}
}
return mixture.ReactionResults[GasReaction.Fire] != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction;
return mixture.ReactionResults[(byte)GasReaction.Fire] != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction;
}
}
}

View File

@@ -96,7 +96,7 @@ public sealed class BeamSystem : SharedBeamSystem
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
_broadphase.RegenerateContacts(ent, physics, manager);
_broadphase.RegenerateContacts((ent, physics, manager));
var distanceLength = distanceCorrection.Length();

View File

@@ -20,6 +20,7 @@ namespace Content.Server.Bed
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SleepingSystem _sleepingSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
@@ -114,7 +115,12 @@ namespace Content.Server.Bed
private void OnEmagged(EntityUid uid, StasisBedComponent component, ref GotEmaggedEvent args)
{
args.Repeatable = true;
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
// Reset any metabolisms first so they receive the multiplier correctly
UpdateMetabolisms(uid, component, false);
component.Multiplier = 1 / component.Multiplier;

View File

@@ -13,6 +13,7 @@ using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
using Content.Shared.HealthExaminable;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;

View File

@@ -7,8 +7,15 @@ namespace Content.Server.Botany.Components;
[RegisterComponent]
public sealed partial class PlantHolderComponent : Component
{
/// <summary>
/// Game time for the next plant reagent update.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary>
/// Time between plant reagent consumption updates.
/// </summary>
[DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3);
@@ -18,18 +25,31 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public int MissingGas;
/// <summary>
/// Time between plant growth updates.
/// </summary>
[DataField]
public TimeSpan CycleDelay = TimeSpan.FromSeconds(15f);
/// <summary>
/// Game time when the plant last did a growth update.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
/// <summary>
/// Sound played when any reagent is transferred into the plant holder.
/// </summary>
[DataField]
public SoundSpecifier? WateringSound;
[DataField]
public bool UpdateSpriteAfterUpdate;
/// <summary>
/// Set to true if the plant holder displays plant warnings (e.g. water low) in the sprite and
/// examine text. Used to differentiate hydroponic trays from simple soil plots.
/// </summary>
[DataField]
public bool DrawWarnings = false;
@@ -60,9 +80,16 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public bool Harvest;
/// <summary>
/// Set to true if this plant has been clipped by seed clippers. Used to prevent a single plant
/// from repeatedly being clipped.
/// </summary>
[DataField]
public bool Sampled;
/// <summary>
/// Multiplier for the number of entities produced at harvest.
/// </summary>
[DataField]
public int YieldMod = 1;
@@ -81,15 +108,28 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public SeedData? Seed;
/// <summary>
/// True if the plant is losing health due to too high/low temperature.
/// </summary>
[DataField]
public bool ImproperHeat;
/// <summary>
/// True if the plant is losing health due to too high/low pressure.
/// </summary>
[DataField]
public bool ImproperPressure;
/// <summary>
/// Not currently used.
/// </summary>
[DataField]
public bool ImproperLight;
/// <summary>
/// Set to true to force a plant update (visuals, component, etc.) regardless of the current
/// update cycle time. Typically used when some interaction affects this plant.
/// </summary>
[DataField]
public bool ForceUpdate;

View File

@@ -10,7 +10,6 @@ using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
@@ -25,10 +24,8 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
public override void Initialize()
@@ -84,7 +81,7 @@ public sealed partial class BotanySystem : EntitySystem
if (!TryGetSeed(component, out var seed))
return;
using (args.PushGroup(nameof(SeedComponent)))
using (args.PushGroup(nameof(SeedComponent), 1))
{
var name = Loc.GetString(seed.DisplayName);
args.PushMarkup(Loc.GetString($"seed-component-description", ("seedName", name)));

View File

@@ -45,5 +45,6 @@ public sealed class LogSystem : EntitySystem
}
QueueDel(uid);
args.Handled = true;
}
}

View File

@@ -22,6 +22,8 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Server.Labels.Components;
using Content.Shared.Containers.ItemSlots;
namespace Content.Server.Botany.Systems;
@@ -39,6 +41,7 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
public const float HydroponicsSpeedMultiplier = 1f;
@@ -176,6 +179,10 @@ public sealed class PlantHolderSystem : EntitySystem
}
component.LastCycle = _gameTiming.CurTime;
if (TryComp<PaperLabelComponent>(args.Used, out var paperLabel))
{
_itemSlots.TryEjectToHands(args.Used, paperLabel.LabelSlot, args.User);
}
QueueDel(args.Used);
CheckLevelSanity(uid, component);

View File

@@ -12,15 +12,22 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
/// <summary>
/// Maximum amount of bounties a station can have.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int MaxBounties = 6;
/// <summary>
/// A list of all the bounties currently active for a station.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public List<CargoBountyData> Bounties = new();
/// <summary>
/// A list of all the bounties that have been completed or
/// skipped for a station.
/// </summary>
[DataField]
public List<CargoBountyHistoryData> History = new();
/// <summary>
/// Used to determine unique order IDs
/// </summary>

View File

@@ -8,6 +8,7 @@ using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.NameIdentifier;
using Content.Shared.Paper;
using Content.Shared.Stacks;
@@ -16,6 +17,7 @@ using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems;
@@ -25,6 +27,7 @@ public sealed partial class CargoSystem
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
@@ -54,7 +57,7 @@ public sealed partial class CargoSystem
return;
var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, untilNextSkip));
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip));
}
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
@@ -95,13 +98,13 @@ public sealed partial class CargoSystem
return;
}
if (!TryRemoveBounty(station, bounty.Value))
if (!TryRemoveBounty(station, bounty.Value, true, args.Actor))
return;
FillBountyDatabase(station);
db.NextSkipTime = _timing.CurTime + db.SkipDelay;
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
_audio.PlayPvs(component.SkipSound, uid);
}
@@ -179,7 +182,7 @@ public sealed partial class CargoSystem
continue;
}
TryRemoveBounty(station, bounty.Value);
TryRemoveBounty(station, bounty.Value, false);
FillBountyDatabase(station);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
}
@@ -434,24 +437,44 @@ public sealed partial class CargoSystem
}
[PublicAPI]
public bool TryRemoveBounty(EntityUid uid, string dataId, StationCargoBountyDatabaseComponent? component = null)
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
string dataId,
bool skipped,
EntityUid? actor = null)
{
if (!TryGetBountyFromId(uid, dataId, out var data, component))
if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp))
return false;
return TryRemoveBounty(uid, data.Value, component);
return TryRemoveBounty(ent, data.Value, skipped, actor);
}
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
CargoBountyData data,
bool skipped,
EntityUid? actor = null)
{
if (!Resolve(uid, ref component))
if (!Resolve(ent, ref ent.Comp))
return false;
for (var i = 0; i < component.Bounties.Count; i++)
for (var i = 0; i < ent.Comp.Bounties.Count; i++)
{
if (component.Bounties[i].Id == data.Id)
if (ent.Comp.Bounties[i].Id == data.Id)
{
component.Bounties.RemoveAt(i);
string? actorName = null;
if (actor != null)
{
var getIdentityEvent = new TryGetIdentityShortInfoEvent(ent.Owner, actor.Value);
RaiseLocalEvent(getIdentityEvent);
actorName = getIdentityEvent.Title;
}
ent.Comp.History.Add(new CargoBountyHistoryData(data,
skipped
? CargoBountyHistoryData.BountyResult.Skipped
: CargoBountyHistoryData.BountyResult.Completed,
_gameTiming.CurTime,
actorName));
ent.Comp.Bounties.RemoveAt(i);
return true;
}
}
@@ -492,7 +515,7 @@ public sealed partial class CargoSystem
}
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
}
}

View File

@@ -8,7 +8,7 @@ using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Paper;
@@ -21,6 +21,7 @@ namespace Content.Server.Cargo.Systems
public sealed partial class CargoSystem
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
@@ -41,6 +42,7 @@ namespace Content.Server.Cargo.Systems
SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated);
SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
Reset();
}
@@ -62,6 +64,7 @@ namespace Content.Server.Cargo.Systems
_audio.PlayPvs(component.ConfirmSound, uid);
UpdateBankAccount(stationUid.Value, bank, (int) price);
QueueDel(args.Used);
args.Handled = true;
}
private void OnInit(EntityUid uid, CargoOrderConsoleComponent orderConsole, ComponentInit args)
@@ -75,6 +78,17 @@ namespace Content.Server.Cargo.Systems
_timer = 0;
}
private void OnEmagged(Entity<CargoOrderConsoleComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent, EmagType.Interaction))
return;
args.Handled = true;
}
private void UpdateConsole(float frameTime)
{
_timer += frameTime;
@@ -85,9 +99,11 @@ namespace Content.Server.Cargo.Systems
{
_timer -= Delay;
foreach (var account in EntityQuery<StationBankAccountComponent>())
var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
while (stationQuery.MoveNext(out var uid, out var bank))
{
account.Balance += account.IncreasePerSecond * Delay;
var balanceToAdd = bank.IncreasePerSecond * Delay;
UpdateBankAccount(uid, bank, balanceToAdd);
}
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
@@ -192,7 +208,7 @@ namespace Content.Server.Cargo.Systems
order.Approved = true;
_audio.PlayPvs(component.ConfirmSound, uid);
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, player);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
@@ -213,7 +229,7 @@ namespace Content.Server.Cargo.Systems
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
orderDatabase.Orders.Remove(order);
DeductFunds(bank, cost);
UpdateBankAccount(station.Value, bank, -cost);
UpdateOrders(station.Value);
}
@@ -536,11 +552,6 @@ namespace Content.Server.Cargo.Systems
}
private void DeductFunds(StationBankAccountComponent component, int amount)
{
component.Balance = Math.Max(0, component.Balance - amount);
}
#region Station
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)

View File

@@ -6,7 +6,7 @@ using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Components;
using Content.Server.Speech.Prototypes;
using Content.Server.Speech.EntitySystems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;

View File

@@ -12,10 +12,7 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Timing;
using Content.Shared.Weapons.Melee.Events;
using Content.Server.Interaction;
using Content.Server.Body.Components;
using Robust.Shared.GameStates;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Server.Audio;
@@ -24,7 +21,6 @@ namespace Content.Server.Chemistry.EntitySystems;
public sealed class HypospraySystem : SharedHypospraySystem
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
public override void Initialize()
{

View File

@@ -1,14 +1,6 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Collections;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.EntitySystems;
@@ -19,16 +11,11 @@ namespace Content.Server.Chemistry.EntitySystems;
public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
}

View File

@@ -60,6 +60,7 @@ namespace Content.Server.Cloning
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
@@ -276,10 +277,15 @@ namespace Content.Server.Cloning
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (!this.IsPowered(uid, EntityManager))
return;
_audio.PlayPvs(clonePod.SparkSound, uid);
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
args.Handled = true;
}
@@ -309,7 +315,7 @@ namespace Content.Server.Cloning
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
@@ -327,7 +333,7 @@ namespace Content.Server.Cloning
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}

View File

@@ -3,18 +3,13 @@ using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Prototypes;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Clothing.Systems;
public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
@@ -22,7 +17,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
base.Initialize();
SubscribeLocalEvent<ChameleonClothingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChameleonClothingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
SubscribeLocalEvent<ChameleonClothingComponent, ChameleonPrototypeSelectedMessage>(OnSelected);
}
@@ -31,40 +25,18 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
SetSelectedPrototype(uid, component.Default, true, component);
}
private void OnVerb(EntityUid uid, ChameleonClothingComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || component.User != args.User)
return;
args.Verbs.Add(new InteractionVerb()
{
Text = Loc.GetString("chameleon-component-verb-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
Act = () => TryOpenUi(uid, args.User, component)
});
}
private void OnSelected(EntityUid uid, ChameleonClothingComponent component, ChameleonPrototypeSelectedMessage args)
{
SetSelectedPrototype(uid, args.SelectedId, component: component);
}
private void TryOpenUi(EntityUid uid, EntityUid user, ChameleonClothingComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (!TryComp(user, out ActorComponent? actor))
return;
_uiSystem.TryToggleUi(uid, ChameleonUiKey.Key, actor.PlayerSession);
}
private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default, component.RequireTag);
_uiSystem.SetUiState(uid, ChameleonUiKey.Key, state);
UI.SetUiState(uid, ChameleonUiKey.Key, state);
}
/// <summary>

View File

@@ -16,7 +16,6 @@ using Content.Shared.Chat;
using Content.Shared.Communications;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Emag.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
@@ -177,7 +176,7 @@ namespace Content.Server.Communications
private bool CanUse(EntityUid user, EntityUid console)
{
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent) && !HasComp<EmaggedComponent>(console))
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent))
{
return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent);
}

View File

@@ -89,6 +89,8 @@ public sealed class FixRotationsCommand : IConsoleCommand
valid |= tagSystem.HasTag(child, "ForceFixRotations");
// override
valid &= !tagSystem.HasTag(child, "ForceNoFixRotations");
// remove diagonal entities as well
valid &= !tagSystem.HasTag(child, "Diagonal");
if (!valid)
continue;

View File

@@ -304,8 +304,8 @@ namespace Content.Server.Construction
return null;
// [Optional] Exit if the new entity's prototype is a parent of the original
// E.g., if an entity with the 'AirlockCommand' prototype was to be replaced with a new entity that
// had the 'Airlock' prototype, and DoNotReplaceInheritingEntities was true, the code block would
// E.g., if an entity with the 'AirlockCommand' prototype was to be replaced with a new entity that
// had the 'Airlock' prototype, and DoNotReplaceInheritingEntities was true, the code block would
// exit here because 'AirlockCommand' is derived from 'Airlock'
if (GetCurrentNode(uid, construction)?.DoNotReplaceInheritingEntities == true &&
metaData.EntityPrototype?.ID != null)
@@ -362,7 +362,7 @@ namespace Content.Server.Construction
// Transform transferring.
var newTransform = Transform(newUid);
newTransform.AttachToGridOrMap(); // in case in hands or a container
TransformSystem.AttachToGridOrMap(newUid, newTransform); // in case in hands or a container
newTransform.LocalRotation = transform.LocalRotation;
newTransform.Anchored = transform.Anchored;

View File

@@ -13,6 +13,8 @@ using Robust.Server.GameObjects;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.IdentityManagement;
using Content.Shared.Security.Components;
using System.Linq;
using Content.Shared.Roles.Jobs;
namespace Content.Server.CriminalRecords.Systems;
@@ -42,6 +44,7 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
subs.Event<CriminalRecordAddHistory>(OnAddHistory);
subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
subs.Event<CriminalRecordSetStatusFilter>(OnStatusFilterPressed);
});
}
@@ -57,6 +60,11 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
ent.Comp.ActiveKey = msg.SelectedKey;
UpdateUserInterface(ent);
}
private void OnStatusFilterPressed(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordSetStatusFilter msg)
{
ent.Comp.FilterStatus = msg.FilterStatus;
UpdateUserInterface(ent);
}
private void OnFiltersChanged(Entity<CriminalRecordsConsoleComponent> ent, ref SetStationRecordFilter msg)
{
@@ -112,13 +120,26 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
}
// will probably never fail given the checks above
name = _records.RecordName(key.Value);
officer = Loc.GetString("criminal-records-console-unknown-officer");
var jobName = "Unknown";
_records.TryGetRecord<GeneralStationRecord>(key.Value, out var entry);
if (entry != null)
jobName = entry.JobTitle;
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
if (tryGetIdentityShortInfoEvent.Title != null)
officer = tryGetIdentityShortInfoEvent.Title;
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
(string, object)[] args;
if (reason != null)
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason) };
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason), ("job", jobName) };
else
args = new (string, object)[] { ("name", name), ("officer", officer) };
args = new (string, object)[] { ("name", name), ("officer", officer), ("job", jobName) };
// figure out which radio message to send depending on transition
var statusString = (oldStatus, msg.Status) switch
@@ -193,8 +214,18 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
return;
}
// get the listing of records to display
var listing = _records.BuildListing((owningStation.Value, stationRecords), console.Filter);
// filter the listing by the selected criminal record status
//if NONE, dont filter by status, just show all crew
if (console.FilterStatus != SecurityStatus.None)
{
listing = listing
.Where(x => _records.TryGetRecord<CriminalRecord>(new StationRecordKey(x.Key, owningStation.Value), out var record) && record.Status == console.FilterStatus)
.ToDictionary(x => x.Key, x => x.Value);
}
var state = new CriminalRecordsConsoleState(listing, console.Filter);
if (console.ActiveKey is { } id)
{
@@ -205,6 +236,9 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
state.SelectedKey = id;
}
// Set the Current Tab aka the filter status type for the records list
state.FilterStatus = console.FilterStatus;
_ui.SetUiState(uid, CriminalRecordsConsoleKey.Key, state);
}

View File

@@ -5,10 +5,16 @@ namespace Content.Server.Damage.Components
[RegisterComponent]
public sealed partial class DamageOnLandComponent : Component
{
/// <summary>
/// Should this entity be damaged when it lands regardless of its resistances?
/// </summary>
[DataField("ignoreResistances")]
[ViewVariables(VVAccess.ReadWrite)]
public bool IgnoreResistances = false;
/// <summary>
/// How much damage.
/// </summary>
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;

View File

@@ -1,5 +1,6 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
@@ -47,7 +48,7 @@ public sealed class DamageForceSaySystem : EntitySystem
}
}
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null)
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
@@ -57,7 +58,13 @@ public sealed class DamageForceSaySystem : EntitySystem
_timing.CurTime < component.NextAllowedTime)
return;
var suffix = Loc.GetString(suffixOverride ?? component.ForceSayStringPrefix + _random.Next(1, component.ForceSayStringCount));
var ev = new BeforeForceSayEvent(component.ForceSayStringDataset);
RaiseLocalEvent(uid, ev);
if (!_prototype.TryIndex(ev.Prefix, out var prefixList))
return;
var suffix = Loc.GetString(_random.Pick(prefixList.Values));
// set cooldown & raise event
component.NextAllowedTime = _timing.CurTime + component.Cooldown;
@@ -80,7 +87,7 @@ public sealed class DamageForceSaySystem : EntitySystem
if (!args.FellAsleep)
return;
TryForceSay(uid, component, true, "damage-force-say-sleep");
TryForceSay(uid, component);
AllowNextSpeech(uid);
}

View File

@@ -1,10 +1,12 @@
using Content.Server.Damage.Components;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage;
using Content.Shared.Throwing;
namespace Content.Server.Damage.Systems
{
/// <summary>
/// Damages the thrown item when it lands.
/// </summary>
public sealed class DamageOnLandSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
@@ -13,19 +15,6 @@ namespace Content.Server.Damage.Systems
{
base.Initialize();
SubscribeLocalEvent<DamageOnLandComponent, LandEvent>(DamageOnLand);
SubscribeLocalEvent<DamageOnLandComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
}
/// <summary>
/// Prevent Pacified entities from throwing damaging items.
/// </summary>
private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args)
{
// Allow healing projectiles, forbid any that do damage:
if (ent.Comp.Damage.AnyPositive())
{
args.Cancel("pacified-cannot-throw");
}
}
private void DamageOnLand(EntityUid uid, DamageOnLandComponent component, ref LandEvent args)

View File

@@ -222,6 +222,7 @@ namespace Content.Server.Database
{
var loadout = new RoleLoadout(role.RoleName)
{
EntityName = role.EntityName,
};
foreach (var group in role.Groups)
@@ -319,6 +320,7 @@ namespace Content.Server.Database
var dz = new ProfileRoleLoadout()
{
RoleName = role,
EntityName = loadouts.EntityName ?? string.Empty,
};
foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
@@ -1112,7 +1114,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.SingleOrDefaultAsync());
}
public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
{
await using var db = await GetDb();
@@ -1122,7 +1124,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return;
}
dbPlayer.LastReadRules = date.UtcDateTime;
dbPlayer.LastReadRules = date?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
@@ -1801,6 +1803,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
#endregion
public abstract Task SendNotification(DatabaseNotification notification);
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
// Normalize DateTimes here so they're always Utc. Thanks.
protected abstract DateTime NormalizeDatabaseTime(DateTime time);

View File

@@ -282,7 +282,7 @@ namespace Content.Server.Database
#region Rules
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
Task SetLastReadRules(NetUserId player, DateTimeOffset? time);
#endregion
@@ -350,6 +350,15 @@ namespace Content.Server.Database
/// <param name="notification">The notification to trigger</param>
void InjectTestNotification(DatabaseNotification notification);
/// <summary>
/// Send a notification to all other servers connected to the same database.
/// </summary>
/// <remarks>
/// The local server will receive the sent notification itself again.
/// </remarks>
/// <param name="notification">The notification to send.</param>
Task SendNotification(DatabaseNotification notification);
#endregion
}
@@ -821,7 +830,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetLastReadRules(player));
}
public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
public Task SetLastReadRules(NetUserId player, DateTimeOffset? time)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetLastReadRules(player, time));
@@ -1045,6 +1054,12 @@ namespace Content.Server.Database
HandleDatabaseNotification(notification);
}
public Task SendNotification(DatabaseNotification notification)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SendNotification(notification));
}
private async void HandleDatabaseNotification(DatabaseNotification notification)
{
lock (_notificationHandlers)

View File

@@ -0,0 +1,76 @@
using System.Text.Json;
using Robust.Shared.Asynchronous;
namespace Content.Server.Database;
public static class ServerDbManagerExt
{
/// <summary>
/// Subscribe to a database notification on a specific channel, formatted as JSON.
/// </summary>
/// <param name="dbManager">The database manager to subscribe on.</param>
/// <param name="taskManager">The task manager used to run the main callback on the main thread.</param>
/// <param name="sawmill">Sawmill to log any errors to.</param>
/// <param name="channel">
/// The notification channel to listen on. Only notifications on this channel will be handled.
/// </param>
/// <param name="action">
/// The action to run on the notification data.
/// This runs on the main thread.
/// </param>
/// <param name="earlyFilter">
/// An early filter callback that runs before the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <param name="filter">
/// A filter callback that runs after the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <typeparam name="TData">The type of JSON data to deserialize.</typeparam>
public static void SubscribeToJsonNotification<TData>(
this IServerDbManager dbManager,
ITaskManager taskManager,
ISawmill sawmill,
string channel,
Action<TData> action,
Func<bool>? earlyFilter = null,
Func<TData, bool>? filter = null)
{
dbManager.SubscribeToNotifications(notification =>
{
if (notification.Channel != channel)
return;
if (notification.Payload == null)
{
sawmill.Error($"Got {channel} notification with null payload!");
return;
}
if (earlyFilter != null && !earlyFilter())
return;
TData data;
try
{
data = JsonSerializer.Deserialize<TData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
sawmill.Error($"Got invalid JSON in {channel} notification: {e}");
return;
}
if (filter != null && !filter(data))
return;
taskManager.RunOnMainThread(() =>
{
action(data);
});
});
}
}

View File

@@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Content.Server.Database;
@@ -17,6 +18,7 @@ public sealed partial class ServerDbPostgres
private static readonly string[] NotificationChannels =
[
BanManager.BanNotificationChannel,
MultiServerKickManager.NotificationChannel,
];
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
@@ -111,6 +113,14 @@ public sealed partial class ServerDbPostgres
});
}
public override async Task SendNotification(DatabaseNotification notification)
{
await using var db = await GetDbImpl();
await db.PgDbContext.Database.ExecuteSqlAsync(
$"SELECT pg_notify({notification.Channel}, {notification.Payload})");
}
public override void Shutdown()
{
_notificationTokenSource.Cancel();

View File

@@ -537,6 +537,12 @@ namespace Content.Server.Database
return await base.AddAdminMessage(message);
}
public override Task SendNotification(DatabaseNotification notification)
{
// Notifications not implemented on SQLite.
return Task.CompletedTask;
}
protected override DateTime NormalizeDatabaseTime(DateTime time)
{
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);

View File

@@ -1,28 +1,5 @@
using Content.Shared.Dice;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.Dice;
[UsedImplicitly]
public sealed class DiceSystem : SharedDiceSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Roll(EntityUid uid, DiceComponent? die = null)
{
if (!Resolve(uid, ref die))
return;
var roll = _random.Next(1, die.Sides + 1);
SetCurrentSide(uid, roll, die);
_popup.PopupEntity(Loc.GetString("dice-component-on-roll-land", ("die", uid), ("currentSide", die.CurrentValue)), uid);
_audio.PlayPvs(die.Sound, uid);
}
}
public sealed class DiceSystem : SharedDiceSystem;

View File

@@ -1,9 +1,7 @@
using Content.Server.GameTicking;
using Content.Server.Voting;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -11,7 +9,6 @@ namespace Content.Server.Discord.WebhookMessages;
public sealed class VoteWebhooks : IPostInjectInit
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntitySystemManager _entSys = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;

View File

@@ -18,21 +18,12 @@ public sealed class AirlockSystem : SharedAirlockSystem
{
base.Initialize();
SubscribeLocalEvent<AirlockComponent, ComponentInit>(OnAirlockInit);
SubscribeLocalEvent<AirlockComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new[] { typeof(DoorSystem) });
}
private void OnAirlockInit(EntityUid uid, AirlockComponent component, ComponentInit args)
{
if (TryComp<ApcPowerReceiverComponent>(uid, out var receiverComponent))
{
Appearance.SetData(uid, DoorVisuals.Powered, receiverComponent.Powered);
}
}
private void OnSignalReceived(EntityUid uid, AirlockComponent component, ref SignalReceivedEvent args)
{
if (args.Port == component.AutoClosePort && component.AutoClose)
@@ -47,11 +38,6 @@ public sealed class AirlockSystem : SharedAirlockSystem
component.Powered = args.Powered;
Dirty(uid, component);
if (TryComp<AppearanceComponent>(uid, out var appearanceComponent))
{
Appearance.SetData(uid, DoorVisuals.Powered, args.Powered, appearanceComponent);
}
if (!TryComp(uid, out DoorComponent? door))
return;

View File

@@ -37,8 +37,6 @@ namespace Content.Server.Doors.Systems
private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args)
{
// TODO this should REALLLLY not be door specific appearance thing.
_appearance.SetData(uid, DoorVisuals.Powered, args.Powered);
component.Powered = args.Powered;
Dirty(uid, component);
}

View File

@@ -15,7 +15,6 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// The type of explosion. Determines damage types and tile break chance scaling.
/// </summary>
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
[JsonIgnore]
public string ExplosionType = default!;
/// <summary>
@@ -23,14 +22,12 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// chance.
/// </summary>
[DataField]
[JsonIgnore]
public float MaxIntensity = 5;
/// <summary>
/// How quickly intensity drops off as you move away from the epicenter
/// </summary>
[DataField]
[JsonIgnore]
public float IntensitySlope = 1;
/// <summary>
@@ -41,15 +38,20 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles.
/// </remarks>
[DataField]
[JsonIgnore]
public float MaxTotalIntensity = 100;
/// <summary>
/// The intensity of the explosion per unit reaction.
/// </summary>
[DataField]
[JsonIgnore]
public float IntensityPerUnit = 1;
/// <summary>
/// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger
/// explosives that don't space tiles, without having to create a new explosion-type prototype.
/// </summary>
[DataField]
public float TileBreakScale = 1f;
public override bool ShouldLog => true;
@@ -72,6 +74,7 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
ExplosionType,
intensity,
IntensitySlope,
MaxIntensity);
MaxIntensity,
TileBreakScale);
}
}

View File

@@ -38,7 +38,8 @@ namespace Content.Server.EntityEffects.Effects
reagentArgs.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.TargetEntity, quantity * multiplier, flammable);
if (reagentArgs.Reagent != null)
reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity);
} else
}
else
{
args.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.TargetEntity, multiplier, flammable);
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
@@ -19,13 +20,17 @@ public sealed partial class Ignite : EntityEffect
public override void Effect(EntityEffectBaseArgs args)
{
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable))
return;
var flamSys = args.EntityManager.System<FlammableSystem>();
if (args is EntityEffectReagentArgs reagentArgs)
{
flamSys.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity);
} else
flamSys.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity, flammable: flammable);
}
else
{
flamSys.Ignite(args.TargetEntity, args.TargetEntity);
flamSys.Ignite(args.TargetEntity, args.TargetEntity, flammable: flammable);
}
}
}

View File

@@ -152,6 +152,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBanManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().PostInit();
IoCManager.Resolve<MultiServerKickManager>().Initialize();
}
}

View File

@@ -69,14 +69,18 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
return;
_needToTransform = true;
var transform = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Grid.Owner);
var size = (float) Grid.TileSize;
var entityManager = IoCManager.Resolve<IEntityManager>();
var transformSystem = entityManager.System<SharedTransformSystem>();
var transform = entityManager.GetComponent<TransformComponent>(Grid.Owner);
var size = (float)Grid.TileSize;
_matrix.M31 = size / 2;
_matrix.M32 = size / 2;
Matrix3x2.Invert(spaceMatrix, out var invSpace);
_matrix *= transform.WorldMatrix * invSpace;
var relativeAngle = transform.WorldRotation - spaceAngle;
var (_, relativeAngle, worldMatrix) = transformSystem.GetWorldPositionRotationMatrix(transform);
relativeAngle -= spaceAngle;
_matrix *= worldMatrix * invSpace;
_offset = relativeAngle.RotateVec(new Vector2(size / 4, size / 4));
}

View File

@@ -71,8 +71,7 @@ public sealed partial class ExplosionSystem
{
var targetGrid = Comp<MapGridComponent>(referenceGrid.Value);
var xform = Transform(referenceGrid.Value);
targetAngle = xform.WorldRotation;
targetMatrix = xform.InvWorldMatrix;
(_, targetAngle, targetMatrix) = _transformSystem.GetWorldPositionRotationInvMatrix(xform);
tileSize = targetGrid.TileSize;
}

View File

@@ -89,8 +89,7 @@ public sealed partial class ExplosionSystem
if (referenceGrid != null)
{
var xform = Transform(Comp<MapGridComponent>(referenceGrid.Value).Owner);
spaceMatrix = xform.WorldMatrix;
spaceAngle = xform.WorldRotation;
(_, spaceAngle, spaceMatrix) = _transformSystem.GetWorldPositionRotationMatrix(xform);
}
// is the explosion starting on a grid?

View File

@@ -3,10 +3,8 @@ using System.Numerics;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Server.Chat.Managers;
using Content.Server.Explosion.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Armor;
using Content.Shared.Camera;
using Content.Shared.CCVar;
using Content.Shared.Damage;
@@ -18,8 +16,6 @@ using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Projectiles;
using Content.Shared.Throwing;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.EntitySystems;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;

View File

@@ -36,7 +36,7 @@ public sealed partial class TriggerSystem
// Re-check for contacts as we cleared them.
else if (TryComp<PhysicsComponent>(uid, out var body))
{
_broadphase.RegenerateContacts(uid, body);
_broadphase.RegenerateContacts((uid, body));
}
}

View File

@@ -126,7 +126,7 @@ namespace Content.Server.Explosion.EntitySystems
private void HandleShockTrigger(Entity<ShockOnTriggerComponent> shockOnTrigger, ref TriggerEvent args)
{
if (!_container.TryGetContainingContainer(shockOnTrigger, out var container))
if (!_container.TryGetContainingContainer(shockOnTrigger.Owner, out var container))
return;
var containerEnt = container.Owner;

View File

@@ -50,6 +50,7 @@ public sealed class FaxSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private const string PaperSlotId = "Paper";
@@ -227,7 +228,7 @@ public sealed class FaxSystem : EntitySystem
return;
}
if (component.KnownFaxes.ContainsValue(newName) && !HasComp<EmaggedComponent>(uid)) // Allow existing names if emagged for fun
if (component.KnownFaxes.ContainsValue(newName) && !_emag.CheckFlag(uid, EmagType.Interaction)) // Allow existing names if emagged for fun
{
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-exist"), uid);
return;
@@ -246,7 +247,12 @@ public sealed class FaxSystem : EntitySystem
private void OnEmagged(EntityUid uid, FaxMachineComponent component, ref GotEmaggedEvent args)
{
_audioSystem.PlayPvs(component.EmagSound, uid);
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}
@@ -260,7 +266,7 @@ public sealed class FaxSystem : EntitySystem
switch (command)
{
case FaxConstants.FaxPingCommand:
var isForSyndie = HasComp<EmaggedComponent>(uid) &&
var isForSyndie = _emag.CheckFlag(uid, EmagType.Interaction) &&
args.Data.ContainsKey(FaxConstants.FaxSyndicateData);
if (!isForSyndie && !component.ResponsePings)
return;
@@ -405,7 +411,7 @@ public sealed class FaxSystem : EntitySystem
{ DeviceNetworkConstants.Command, FaxConstants.FaxPingCommand }
};
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
payload.Add(FaxConstants.FaxSyndicateData, true);
_deviceNetworkSystem.QueuePacket(uid, null, payload);
@@ -445,6 +451,9 @@ public sealed class FaxSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
if (component.SendTimeoutRemaining > 0)
return;
var sendEntity = component.PaperSlot.Item;
if (sendEntity == null)
return;
@@ -489,6 +498,9 @@ public sealed class FaxSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
if (component.SendTimeoutRemaining > 0)
return;
var sendEntity = component.PaperSlot.Item;
if (sendEntity == null)
return;

View File

@@ -84,7 +84,7 @@ public sealed class PuddleDebugDebugOverlaySystem : SharedPuddleDebugOverlaySyst
data.Add(new PuddleDebugOverlayData(pos, vol));
}
RaiseNetworkEvent(new PuddleOverlayDebugMessage(GetNetEntity(gridUid), data.ToArray()));
RaiseNetworkEvent(new PuddleOverlayDebugMessage(GetNetEntity(gridUid), data.ToArray()), session);
}
}

View File

@@ -228,7 +228,7 @@ public sealed class SmokeSystem : EntitySystem
var xform = Transform(uid);
_physics.SetBodyType(uid, BodyType.Dynamic, fixtures, body, xform);
_physics.SetCanCollide(uid, true, manager: fixtures, body: body);
_broadphase.RegenerateContacts(uid, body, fixtures, xform);
_broadphase.RegenerateContacts((uid, body, fixtures, xform));
}
var timer = EnsureComp<TimedDespawnComponent>(uid);

View File

@@ -1,11 +0,0 @@
namespace Content.Server.Forensics;
/// <summary>
/// This component is for mobs that have DNA.
/// </summary>
[RegisterComponent]
public sealed partial class DnaComponent : Component
{
[DataField("dna"), ViewVariables(VVAccess.ReadWrite)]
public string DNA = String.Empty;
}

View File

@@ -17,7 +17,6 @@ namespace Content.Server.Forensics
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly LabelSystem _label = default!;
public override void Initialize()

View File

@@ -10,6 +10,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.DoAfter;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using System.Linq;
using System.Numerics;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking.Events;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
@@ -27,6 +28,7 @@ namespace Content.Server.GameTicking
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly AdminSystem _admin = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string ObserverPrototypeName = "MobObserver";
@@ -233,6 +235,7 @@ namespace Content.Server.GameTicking
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_admin.UpdatePlayerList(player);
if (lateJoin && !silent)
{
@@ -420,7 +423,7 @@ namespace Content.Server.GameTicking
{
var gridXform = Transform(gridUid);
return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, gridXform.InvWorldMatrix));
return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, _transform.GetInvWorldMatrix(gridXform)));
}
return spawn;

View File

@@ -57,7 +57,6 @@ namespace Content.Server.Ghost
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
@@ -503,7 +502,7 @@ namespace Content.Server.Ghost
var playerEntity = mind.CurrentEntity;
if (playerEntity != null && viaCommand)
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
RaiseLocalEvent(handleEv);
@@ -575,7 +574,7 @@ namespace Content.Server.Ghost
}
if (playerEntity != null)
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}");
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}");
var ghost = SpawnGhost((mindId, mind), position, canReturn);

View File

@@ -15,6 +15,7 @@ using Content.Shared.Telephone;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -35,22 +36,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
private const float MinTimeBetweenSyncRequests = 0.5f;
private TimeSpan _minTimeSpanBetweenSyncRequests;
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
public override void Initialize()
{
base.Initialize();
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
// Holopad UI and bound user interface messages
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
@@ -68,7 +62,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Networked events
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
// Component start/shutdown events
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
@@ -268,16 +261,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (source.Comp.Hologram == null)
GenerateHologram(source);
// Receiver holopad holograms have to be generated now instead of waiting for their own event
// to fire because holographic avatars get synced immediately
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
GenerateHologram((args.Receiver, receivingHolopad));
if (source.Comp.User != null)
{
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User.Value);
}
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User);
}
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
@@ -323,22 +311,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
{
var uid = args.SenderSession.AttachedEntity;
if (!Exists(uid))
return;
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
return;
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
return;
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
}
#endregion
#region: Component start/shutdown events
@@ -485,8 +457,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
}
_recentlyUpdatedHolograms.Clear();
}
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
@@ -555,10 +525,16 @@ public sealed class HolopadSystem : SharedHolopadSystem
QueueDel(hologram);
}
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid? user)
{
if (user == null)
{
UnlinkHolopadFromUser(entity, null);
return;
}
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
holopadUser = AddComp<HolopadUserComponent>(user);
holopadUser = AddComp<HolopadUserComponent>(user.Value);
if (user != entity.Comp.User?.Owner)
{
@@ -567,51 +543,44 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Assigns the new user in their place
holopadUser.LinkedHolopads.Add(entity);
entity.Comp.User = (user, holopadUser);
entity.Comp.User = (user.Value, holopadUser);
}
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
{
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
return;
}
// We have no apriori sprite data for the hologram, request
// the current appearance of the user from the client
RequestHolopadUserSpriteUpdate((user, holopadUser));
// Add the new user to PVS and sync their appearance with any
// holopads connected to the one they are using
_pvs.AddGlobalOverride(user.Value);
SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
}
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
if (user == null)
return;
entity.Comp.User = null;
SyncHolopadHologramAppearanceWithTarget(entity, null);
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram != null)
{
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
// Send message with no sprite data to the client
// This will set the holgram sprite to a generic icon
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
RaiseNetworkEvent(ev);
}
}
if (!HasComp<HolopadUserComponent>(user))
if (user == null)
return;
user.Value.Comp.LinkedHolopads.Remove(entity);
if (!user.Value.Comp.LinkedHolopads.Any())
if (!user.Value.Comp.LinkedHolopads.Any() &&
user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
{
_pendingRequestsForSpriteState.Remove(user.Value);
_pvs.RemoveGlobalOverride(user.Value);
RemComp<HolopadUserComponent>(user.Value);
}
}
private void SyncHolopadHologramAppearanceWithTarget(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram == null)
continue;
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
RemComp<HolopadUserComponent>(user.Value);
if (user == null)
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
Dirty(linkedHolopad.Comp.Hologram.Value);
}
}
@@ -646,31 +615,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
Dirty(entity);
}
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
{
if (!_pendingRequestsForSpriteState.Add(user))
return;
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
RaiseNetworkEvent(ev);
}
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
{
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
{
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
{
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
continue;
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
RaiseNetworkEvent(ev);
}
}
}
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
{
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))

View File

@@ -20,40 +20,6 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
SubscribeLocalEvent<HumanoidAppearanceComponent, GetVerbsEvent<Verb>>(OnVerbsRequest);
}
// this was done enough times that it only made sense to do it here
/// <summary>
/// Clones a humanoid's appearance to a target mob, provided they both have humanoid components.
/// </summary>
/// <param name="source">Source entity to fetch the original appearance from.</param>
/// <param name="target">Target entity to apply the source entity's appearance to.</param>
/// <param name="sourceHumanoid">Source entity's humanoid component.</param>
/// <param name="targetHumanoid">Target entity's humanoid component.</param>
public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearanceComponent? sourceHumanoid = null,
HumanoidAppearanceComponent? targetHumanoid = null)
{
if (!Resolve(source, ref sourceHumanoid) || !Resolve(target, ref targetHumanoid))
{
return;
}
targetHumanoid.Species = sourceHumanoid.Species;
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
targetHumanoid.Age = sourceHumanoid.Age;
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
targetHumanoid.Gender = sourceHumanoid.Gender;
if (TryComp<GrammarComponent>(target, out var grammar))
{
grammar.Gender = sourceHumanoid.Gender;
}
Dirty(target, targetHumanoid);
}
/// <summary>
/// Removes a marking from a humanoid by ID.
/// </summary>

View File

@@ -103,6 +103,8 @@ public sealed class IdentitySystem : SharedIdentitySystem
// If presumed name is null and we're using that, we set proper noun to be false ("the old woman")
if (name != representation.TrueName && representation.PresumedName == null)
identityGrammar.ProperNoun = false;
Dirty(ident, identityGrammar);
}
if (name == Name(ident))

View File

@@ -1,4 +1,6 @@
using Content.Shared.Implants.Components;
using Content.Server.Body.Components;
using Content.Shared.Implants.Components;
using Content.Shared.Storage;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
@@ -9,17 +11,29 @@ public sealed partial class ImplanterSystem
{
SubscribeLocalEvent<ImplantedComponent, ComponentInit>(OnImplantedInit);
SubscribeLocalEvent<ImplantedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ImplantedComponent, BeingGibbedEvent>(OnGibbed);
}
private void OnImplantedInit(EntityUid uid, ImplantedComponent component, ComponentInit args)
private void OnImplantedInit(Entity<ImplantedComponent> ent, ref ComponentInit args)
{
component.ImplantContainer = _container.EnsureContainer<Container>(uid, ImplanterComponent.ImplantSlotId);
component.ImplantContainer.OccludesLight = false;
ent.Comp.ImplantContainer = _container.EnsureContainer<Container>(ent.Owner, ImplanterComponent.ImplantSlotId);
ent.Comp.ImplantContainer.OccludesLight = false;
}
private void OnShutdown(EntityUid uid, ImplantedComponent component, ComponentShutdown args)
private void OnShutdown(Entity<ImplantedComponent> ent, ref ComponentShutdown args)
{
//If the entity is deleted, get rid of the implants
_container.CleanContainer(component.ImplantContainer);
_container.CleanContainer(ent.Comp.ImplantContainer);
}
private void OnGibbed(Entity<ImplantedComponent> ent, ref BeingGibbedEvent args)
{
// Drop the storage implant contents before the implants are deleted by the body being gibbed
foreach (var implant in ent.Comp.ImplantContainer.ContainedEntities)
{
if (TryComp<StorageComponent>(implant, out var storage))
_container.EmptyContainer(storage.Container, destination: Transform(ent).Coordinates);
}
}
}

View File

@@ -58,17 +58,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
return;
}
// Check if we are trying to implant a implant which is already implanted
if (implant.HasValue && !component.AllowMultipleImplants && CheckSameImplant(target, implant.Value))
{
var name = Identity.Name(target, EntityManager, args.User);
var msg = Loc.GetString("implanter-component-implant-already", ("implant", implant), ("target", name));
_popup.PopupEntity(msg, target, args.User);
args.Handled = true;
return;
}
//Implant self instantly, otherwise try to inject the target.
if (args.User == target)
Implant(target, target, uid, component);
@@ -79,14 +68,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
args.Handled = true;
}
public bool CheckSameImplant(EntityUid target, EntityUid implant)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
return false;
var implantPrototype = Prototype(implant);
return implanted.ImplantContainer.ContainedEntities.Any(entity => Prototype(entity) == implantPrototype);
}
/// <summary>
/// Attempt to implant someone else.

View File

@@ -0,0 +1,76 @@
using Content.Server.Radio.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
public sealed class RadioImplantSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadioImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
SubscribeLocalEvent<RadioImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
/// <summary>
/// If implanted with a radio implant, installs the necessary intrinsic radio components
/// </summary>
private void OnImplantImplanted(Entity<RadioImplantComponent> ent, ref ImplantImplantedEvent args)
{
if (args.Implanted == null)
return;
var activeRadio = EnsureComp<ActiveRadioComponent>(args.Implanted.Value);
foreach (var channel in ent.Comp.RadioChannels)
{
if (activeRadio.Channels.Add(channel))
ent.Comp.ActiveAddedChannels.Add(channel);
}
EnsureComp<IntrinsicRadioReceiverComponent>(args.Implanted.Value);
var intrinsicRadioTransmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(args.Implanted.Value);
foreach (var channel in ent.Comp.RadioChannels)
{
if (intrinsicRadioTransmitter.Channels.Add(channel))
ent.Comp.TransmitterAddedChannels.Add(channel);
}
}
/// <summary>
/// Removes intrinsic radio components once the Radio Implant is removed
/// </summary>
private void OnRemove(Entity<RadioImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
if (TryComp<ActiveRadioComponent>(args.Container.Owner, out var activeRadioComponent))
{
foreach (var channel in ent.Comp.ActiveAddedChannels)
{
activeRadioComponent.Channels.Remove(channel);
}
ent.Comp.ActiveAddedChannels.Clear();
if (activeRadioComponent.Channels.Count == 0)
{
RemCompDeferred<ActiveRadioComponent>(args.Container.Owner);
}
}
if (!TryComp<IntrinsicRadioTransmitterComponent>(args.Container.Owner, out var radioTransmitterComponent))
return;
foreach (var channel in ent.Comp.TransmitterAddedChannels)
{
radioTransmitterComponent.Channels.Remove(channel);
}
ent.Comp.TransmitterAddedChannels.Clear();
if (radioTransmitterComponent.Channels.Count == 0 || activeRadioComponent?.Channels.Count == 0)
{
RemCompDeferred<IntrinsicRadioTransmitterComponent>(args.Container.Owner);
}
}
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
using Content.Shared.Humanoid;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
@@ -22,6 +23,7 @@ using System.Numerics;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
using Content.Server.IdentityManagement;
using Content.Server.DetailExaminable;
using Content.Shared.Store.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
@@ -225,6 +227,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
{
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
}
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
}

View File

@@ -34,7 +34,7 @@ public sealed class RulesManager
{
PopupTime = _cfg.GetCVar(CCVars.RulesWaitTime),
CoreRules = _cfg.GetCVar(CCVars.RulesFile),
ShouldShowRules = !isLocalhost && !hasCooldown
ShouldShowRules = !isLocalhost && !hasCooldown,
};
_netManager.ServerSendMessage(showRulesMessage, e.Channel);
}

View File

@@ -75,6 +75,7 @@ namespace Content.Server.IoC
IoCManager.Register<MappingManager>();
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
}
}
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.ItemRecall;
namespace Content.Server.ItemRecall;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
public sealed partial class ItemRecallSystem : SharedItemRecallSystem
{
}

View File

@@ -16,8 +16,10 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.UserInterface;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Materials;
using Content.Shared.Power;
using Content.Shared.ReagentSpeed;
@@ -42,6 +44,7 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly PopupSystem _popup = default!;
@@ -55,6 +58,7 @@ namespace Content.Server.Lathe
/// Per-tick cache
/// </summary>
private readonly List<GasMixture> _environments = new();
private readonly HashSet<ProtoId<LatheRecipePrototype>> _availableRecipes = new();
public override void Initialize()
{
@@ -154,19 +158,16 @@ namespace Content.Server.Lathe
public List<ProtoId<LatheRecipePrototype>> GetAvailableRecipes(EntityUid uid, LatheComponent component, bool getUnavailable = false)
{
_availableRecipes.Clear();
AddRecipesFromPacks(_availableRecipes, component.StaticPacks);
var ev = new LatheGetRecipesEvent(uid, getUnavailable)
{
Recipes = new HashSet<ProtoId<LatheRecipePrototype>>(component.StaticRecipes)
Recipes = _availableRecipes
};
RaiseLocalEvent(uid, ev);
return ev.Recipes.ToList();
}
public static List<ProtoId<LatheRecipePrototype>> GetAllBaseRecipes(LatheComponent component)
{
return component.StaticRecipes.Union(component.DynamicRecipes).ToList();
}
public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, LatheComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -275,35 +276,42 @@ namespace Content.Server.Lathe
_uiSys.SetUiState(uid, LatheUiKey.Key, state);
}
/// <summary>
/// Adds every unlocked recipe from each pack to the recipes list.
/// </summary>
public void AddRecipesFromDynamicPacks(ref LatheGetRecipesEvent args, TechnologyDatabaseComponent database, IEnumerable<ProtoId<LatheRecipePackPrototype>> packs)
{
foreach (var id in packs)
{
var pack = _proto.Index(id);
foreach (var recipe in pack.Recipes)
{
if (args.getUnavailable || database.UnlockedRecipes.Contains(recipe))
args.Recipes.Add(recipe);
}
}
}
private void OnGetRecipes(EntityUid uid, TechnologyDatabaseComponent component, LatheGetRecipesEvent args)
{
if (uid != args.Lathe || !TryComp<LatheComponent>(uid, out var latheComponent))
return;
foreach (var recipe in latheComponent.DynamicRecipes)
{
if (!(args.getUnavailable || component.UnlockedRecipes.Contains(recipe)) || args.Recipes.Contains(recipe))
continue;
args.Recipes.Add(recipe);
}
AddRecipesFromDynamicPacks(ref args, component, latheComponent.DynamicPacks);
}
private void GetEmagLatheRecipes(EntityUid uid, EmagLatheRecipesComponent component, LatheGetRecipesEvent args)
{
if (uid != args.Lathe || !TryComp<TechnologyDatabaseComponent>(uid, out var technologyDatabase))
if (uid != args.Lathe)
return;
if (!args.getUnavailable && !HasComp<EmaggedComponent>(uid))
if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
return;
foreach (var recipe in component.EmagDynamicRecipes)
{
if (!(args.getUnavailable || technologyDatabase.UnlockedRecipes.Contains(recipe)) || args.Recipes.Contains(recipe))
continue;
args.Recipes.Add(recipe);
}
foreach (var recipe in component.EmagStaticRecipes)
{
args.Recipes.Add(recipe);
}
AddRecipesFromPacks(args.Recipes, component.EmagStaticPacks);
if (TryComp<TechnologyDatabaseComponent>(uid, out var database))
AddRecipesFromDynamicPacks(ref args, database, component.EmagDynamicPacks);
}
private void OnHeatStartPrinting(EntityUid uid, LatheHeatProducingComponent component, LatheStartPrintingEvent args)

View File

@@ -19,4 +19,17 @@ public sealed class MagicSystem : SharedMagicSystem
{
_chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false);
}
public override void OnVoidApplause(VoidApplauseSpellEvent ev)
{
base.OnVoidApplause(ev);
_chat.TryEmoteWithChat(ev.Performer, ev.Emote);
var perfXForm = Transform(ev.Performer);
var targetXForm = Transform(ev.Target);
Spawn(ev.Effect, perfXForm.Coordinates);
Spawn(ev.Effect, targetXForm.Coordinates);
}
}

View File

@@ -6,7 +6,7 @@ using Content.Server.EUI;
using Content.Server.Ghost;
using Content.Server.Popups;
using Content.Server.PowerCell;
using Content.Server.Traits.Assorted;
using Content.Shared.Traits.Assorted;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
@@ -193,9 +193,9 @@ public sealed class DefibrillatorSystem : EntitySystem
_chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-rotten"),
InGameICChatType.Speak, true);
}
else if (HasComp<UnrevivableComponent>(target))
else if (TryComp<UnrevivableComponent>(target, out var unrevivable))
{
_chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-unrevivable"),
_chatManager.TrySendInGameICMessage(uid, Loc.GetString(unrevivable.ReasonMessage),
InGameICChatType.Speak, true);
}
else

View File

@@ -72,7 +72,10 @@ public sealed class HealingSystem : EntitySystem
_bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
if (isBleeding != bloodstream.BleedAmount > 0)
{
_popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User);
var popup = (args.User == entity.Owner)
? Loc.GetString("medical-item-stop-bleeding-self")
: Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
_popupSystem.PopupEntity(popup, entity, args.User);
}
}
@@ -115,15 +118,15 @@ public sealed class HealingSystem : EntitySystem
_audio.PlayPvs(healing.HealingEndSound, entity.Owner, AudioHelpers.WithVariation(0.125f, _random).WithVolume(1f));
// Logic to determine the whether or not to repeat the healing action
args.Repeat = (HasDamage(entity.Comp, healing) && !dontRepeat);
args.Repeat = (HasDamage(entity, healing) && !dontRepeat);
if (!args.Repeat && !dontRepeat)
_popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User);
args.Handled = true;
}
private bool HasDamage(DamageableComponent component, HealingComponent healing)
private bool HasDamage(Entity<DamageableComponent> ent, HealingComponent healing)
{
var damageableDict = component.Damage.DamageDict;
var damageableDict = ent.Comp.Damage.DamageDict;
var healingDict = healing.Damage.DamageDict;
foreach (var type in healingDict)
{
@@ -133,6 +136,23 @@ public sealed class HealingSystem : EntitySystem
}
}
if (TryComp<BloodstreamComponent>(ent, out var bloodstream))
{
// Is ent missing blood that we can restore?
if (healing.ModifyBloodLevel > 0
&& _solutionContainerSystem.ResolveSolution(ent.Owner, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
&& bloodSolution.Volume < bloodSolution.MaxVolume)
{
return true;
}
// Is ent bleeding and can we stop it?
if (healing.BloodlossModifier < 0 && bloodstream.BleedAmount > 0)
{
return true;
}
}
return false;
}
@@ -172,14 +192,7 @@ public sealed class HealingSystem : EntitySystem
if (TryComp<StackComponent>(uid, out var stack) && stack.Count < 1)
return false;
var anythingToDo =
HasDamage(targetDamage, component) ||
component.ModifyBloodLevel > 0 // Special case if healing item can restore lost blood...
&& TryComp<BloodstreamComponent>(target, out var bloodstream)
&& _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
&& bloodSolution.Volume < bloodSolution.MaxVolume; // ...and there is lost blood to restore.
if (!anythingToDo)
if (!HasDamage((target, targetDamage), component))
{
_popupSystem.PopupEntity(Loc.GetString("medical-item-cant-use", ("item", uid)), uid, user);
return false;

View File

@@ -2,7 +2,7 @@ using Content.Server.Body.Components;
using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Server.Temperature.Components;
using Content.Server.Traits.Assorted;
using Content.Shared.Traits.Assorted;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
@@ -207,7 +207,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
bleeding = bloodstream.BleedAmount > 0;
}
if (HasComp<UnrevivableComponent>(target))
if (TryComp<UnrevivableComponent>(target, out var unrevivableComp) && unrevivableComp.Analyzable)
unrevivable = true;
_uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage(

View File

@@ -22,6 +22,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -42,6 +43,7 @@ public sealed class SuitSensorSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
@@ -369,7 +371,7 @@ public sealed class SuitSensorSystem : EntitySystem
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString($"department-{department}"));
userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
}
// get health mob state

View File

@@ -35,7 +35,7 @@ public sealed class MiningSystem : EntitySystem
return;
var coords = Transform(uid).Coordinates;
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield);
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield+1);
for (var i = 0; i < toSpawn; i++)
{
Spawn(proto.OreEntity, coords.Offset(_random.NextVector2(0.2f)));

View File

@@ -0,0 +1,12 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates;
namespace Content.Server.Movement.Components;
[RegisterComponent]
public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
{
}

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