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

# Conflicts:
#	Resources/Prototypes/Maps/Pools/default.yml
This commit is contained in:
Ed
2024-05-30 13:29:19 +03:00
242 changed files with 8716 additions and 12827 deletions

View File

@@ -1,27 +0,0 @@
using Content.Shared.Cabinet;
using Robust.Client.GameObjects;
namespace Content.Client.Cabinet;
public sealed class ItemCabinetSystem : SharedItemCabinetSystem
{
protected override void UpdateAppearance(EntityUid uid, ItemCabinetComponent? cabinet = null)
{
if (!Resolve(uid, ref cabinet))
return;
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
var state = cabinet.Opened ? cabinet.OpenState : cabinet.ClosedState;
if (state != null)
sprite.LayerSetState(ItemCabinetVisualLayers.Door, state);
sprite.LayerSetVisible(ItemCabinetVisualLayers.ContainsItem, cabinet.CabinetSlot.HasItem);
}
}
public enum ItemCabinetVisualLayers
{
Door,
ContainsItem
}

View File

@@ -73,11 +73,6 @@ public sealed class DragDropHelper<T>
_cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
}
~DragDropHelper()
{
_cfg.UnsubValueChanged(CCVars.DragDropDeadZone, SetDeadZone);
}
/// <summary>
/// Tell the helper that the mouse button was pressed down on
/// a target, thus a drag has the possibility to begin for this target.

View File

@@ -4,10 +4,11 @@
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'observe-warning-1'}"/>
<Label Text="{Loc 'observe-warning-2'}"/>
<BoxContainer Orientation="Horizontal" >
<BoxContainer Orientation="Horizontal">
<Button Name="NevermindButton" Text="{Loc 'observe-nevermind'}" SizeFlagsStretchRatio="1"/>
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2" />
<cc:CommandButton Command="observe" Name="ObserveButton" StyleClasses="Caution" Text="{Loc 'observe-confirm'}" SizeFlagsStretchRatio="1"/>
<cc:CommandButton Command="observe" Name="ObserveButton" StyleClasses="Caution" Text="{Loc 'observe-confirm'}" SizeFlagsStretchRatio="1"/>
<cc:CommandButton Command="observe admin" Name="ObserveAsAdminButton" Text="{Loc 'observe-as-admin'}" SizeFlagsStretchRatio="1" Visible="False"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,5 +1,7 @@
using Content.Shared.Administration.Managers;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Player;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -9,11 +11,22 @@ namespace Content.Client.Lobby.UI;
[UsedImplicitly]
public sealed partial class ObserveWarningWindow : DefaultWindow
{
[Dependency] private readonly ISharedAdminManager _adminManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public ObserveWarningWindow()
{
Title = Loc.GetString("observe-warning-window-title");
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var player = _playerManager.LocalSession;
if (player != null && _adminManager.IsAdmin(player))
{
ObserveButton.Text = Loc.GetString("observe-as-player");
ObserveAsAdminButton.Visible = true;
ObserveAsAdminButton.OnPressed += _ => { this.Close(); };
}
ObserveButton.OnPressed += _ => { this.Close(); };
NevermindButton.OnPressed += _ => { this.Close(); };

View File

@@ -10,51 +10,56 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<FloorOcclusionComponent, ComponentStartup>(OnOcclusionStartup);
SubscribeLocalEvent<FloorOcclusionComponent, ComponentShutdown>(OnOcclusionShutdown);
SubscribeLocalEvent<FloorOcclusionComponent, AfterAutoHandleStateEvent>(OnOcclusionAuto);
}
private void OnOcclusionAuto(EntityUid uid, FloorOcclusionComponent component, ref AfterAutoHandleStateEvent args)
private void OnOcclusionAuto(Entity<FloorOcclusionComponent> ent, ref AfterAutoHandleStateEvent args)
{
SetEnabled(uid, component, component.Enabled);
SetShader(ent.Owner, ent.Comp.Enabled);
}
private void OnOcclusionStartup(EntityUid uid, FloorOcclusionComponent component, ComponentStartup args)
private void OnOcclusionStartup(Entity<FloorOcclusionComponent> ent, ref ComponentStartup args)
{
if (component.Enabled && TryComp<SpriteComponent>(uid, out var sprite))
SetShader(sprite, true);
SetShader(ent.Owner, ent.Comp.Enabled);
}
protected override void SetEnabled(EntityUid uid, FloorOcclusionComponent component, bool enabled)
private void OnOcclusionShutdown(Entity<FloorOcclusionComponent> ent, ref ComponentShutdown args)
{
if (component.Enabled == enabled)
SetShader(ent.Owner, false);
}
protected override void SetEnabled(Entity<FloorOcclusionComponent> entity)
{
SetShader(entity.Owner, entity.Comp.Enabled);
}
private void SetShader(Entity<SpriteComponent?> sprite, bool enabled)
{
if (!_spriteQuery.Resolve(sprite.Owner, ref sprite.Comp, false))
return;
base.SetEnabled(uid, component, enabled);
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
SetShader(sprite, enabled);
}
private void SetShader(SpriteComponent sprite, bool enabled)
{
var shader = _proto.Index<ShaderPrototype>("HorizontalCut").Instance();
if (sprite.PostShader is not null && sprite.PostShader != shader)
if (sprite.Comp.PostShader is not null && sprite.Comp.PostShader != shader)
return;
if (enabled)
{
sprite.PostShader = shader;
sprite.Comp.PostShader = shader;
}
else
{
sprite.PostShader = null;
sprite.Comp.PostShader = null;
}
}
}

View File

@@ -7,6 +7,5 @@
<tabs:GraphicsTab Name="GraphicsTab" />
<tabs:KeyRebindTab Name="KeyRebindTab" />
<tabs:AudioTab Name="AudioTab" />
<tabs:NetworkTab Name="NetworkTab" />
</TabContainer>
</DefaultWindow>

View File

@@ -19,7 +19,6 @@ namespace Content.Client.Options.UI
Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-graphics"));
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-network"));
UpdateTabs();
}

View File

@@ -1,102 +0,0 @@
<Control xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Content.Client.Options.UI.Tabs.NetworkTab">
<BoxContainer Orientation="Vertical" >
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<CheckBox Name="NetPredictCheckbox" Text="{Loc 'ui-options-net-predict'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-interp-ratio'}" />
<Control MinSize="8 0" />
<Slider Name="NetInterpRatioSlider"
ToolTip="{Loc 'ui-options-net-interp-ratio-tooltip'}"
MaxValue="8"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetInterpRatioLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-predict-tick-bias'}" />
<Control MinSize="8 0" />
<Slider Name="NetPredictTickBiasSlider"
ToolTip="{Loc 'ui-options-net-predict-tick-bias-tooltip'}"
MaxValue="6"
MinValue="0"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPredictTickBiasLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-pvs-spawn'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsSpawnSlider"
ToolTip="{Loc 'ui-options-net-pvs-spawn-tooltip'}"
MaxValue="150"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsSpawnLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 0">
<Label Text="{Loc 'ui-options-net-pvs-entry'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsEntrySlider"
ToolTip="{Loc 'ui-options-net-pvs-entry-tooltip'}"
MaxValue="500"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsEntryLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="4 10 4 10">
<Label Text="{Loc 'ui-options-net-pvs-leave'}" />
<Control MinSize="8 0" />
<Slider Name="NetPvsLeaveSlider"
ToolTip="{Loc 'ui-options-net-pvs-leave-tooltip'}"
MaxValue="300"
MinValue="20"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="NetPvsLeaveLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
</BoxContainer>
<controls:StripeBack HasBottomEdge="False" HasMargins="False">
<BoxContainer Orientation="Horizontal"
Align="End"
HorizontalExpand="True"
VerticalExpand="True">
<Button Name="ResetButton"
Text="{Loc 'ui-options-reset-all'}"
StyleClasses="Caution"
HorizontalExpand="True"
HorizontalAlignment="Right" />
<Button Name="DefaultButton"
Text="{Loc 'ui-options-default'}"
TextAlign="Center"
HorizontalAlignment="Right" />
<Control MinSize="2 0" />
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
TextAlign="Center"
HorizontalAlignment="Right" />
</BoxContainer>
</controls:StripeBack>
</BoxContainer>
</Control>

View File

@@ -1,125 +0,0 @@
using System.Globalization;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Client.GameStates;
using Content.Client.Entry;
namespace Content.Client.Options.UI.Tabs
{
[GenerateTypedNameReferences]
public sealed partial class NetworkTab : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientGameStateManager _stateMan = default!;
public NetworkTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ApplyButton.OnPressed += OnApplyButtonPressed;
ResetButton.OnPressed += OnResetButtonPressed;
DefaultButton.OnPressed += OnDefaultButtonPressed;
NetPredictCheckbox.OnToggled += OnPredictToggled;
NetInterpRatioSlider.OnValueChanged += OnSliderChanged;
NetInterpRatioSlider.MinValue = _stateMan.MinBufferSize;
NetPredictTickBiasSlider.OnValueChanged += OnSliderChanged;
NetPvsSpawnSlider.OnValueChanged += OnSliderChanged;
NetPvsEntrySlider.OnValueChanged += OnSliderChanged;
NetPvsLeaveSlider.OnValueChanged += OnSliderChanged;
Reset();
}
protected override void Dispose(bool disposing)
{
ApplyButton.OnPressed -= OnApplyButtonPressed;
ResetButton.OnPressed -= OnResetButtonPressed;
DefaultButton.OnPressed -= OnDefaultButtonPressed;
NetPredictCheckbox.OnToggled -= OnPredictToggled;
NetInterpRatioSlider.OnValueChanged -= OnSliderChanged;
NetPredictTickBiasSlider.OnValueChanged -= OnSliderChanged;
NetPvsSpawnSlider.OnValueChanged -= OnSliderChanged;
NetPvsEntrySlider.OnValueChanged -= OnSliderChanged;
NetPvsLeaveSlider.OnValueChanged -= OnSliderChanged;
base.Dispose(disposing);
}
private void OnPredictToggled(BaseButton.ButtonToggledEventArgs obj)
{
UpdateChanges();
}
private void OnSliderChanged(Robust.Client.UserInterface.Controls.Range range)
{
UpdateChanges();
}
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
{
_cfg.SetCVar(CVars.NetBufferSize, (int) NetInterpRatioSlider.Value - _stateMan.MinBufferSize);
_cfg.SetCVar(CVars.NetPredictTickBias, (int) NetPredictTickBiasSlider.Value);
_cfg.SetCVar(CVars.NetPVSEntityBudget, (int) NetPvsSpawnSlider.Value);
_cfg.SetCVar(CVars.NetPVSEntityEnterBudget, (int) NetPvsEntrySlider.Value);
_cfg.SetCVar(CVars.NetPVSEntityExitBudget, (int) NetPvsLeaveSlider.Value);
_cfg.SetCVar(CVars.NetPredict, NetPredictCheckbox.Pressed);
_cfg.SaveToFile();
UpdateChanges();
}
private void OnResetButtonPressed(BaseButton.ButtonEventArgs args)
{
Reset();
}
private void OnDefaultButtonPressed(BaseButton.ButtonEventArgs obj)
{
NetPredictTickBiasSlider.Value = CVars.NetPredictTickBias.DefaultValue;
NetPvsSpawnSlider.Value = CVars.NetPVSEntityBudget.DefaultValue;
NetPvsEntrySlider.Value = CVars.NetPVSEntityEnterBudget.DefaultValue;
NetPvsLeaveSlider.Value = CVars.NetPVSEntityExitBudget.DefaultValue;
NetInterpRatioSlider.Value = CVars.NetBufferSize.DefaultValue + _stateMan.MinBufferSize;
UpdateChanges();
}
private void Reset()
{
NetInterpRatioSlider.Value = _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize;
NetPredictTickBiasSlider.Value = _cfg.GetCVar(CVars.NetPredictTickBias);
NetPvsSpawnSlider.Value = _cfg.GetCVar(CVars.NetPVSEntityBudget);
NetPvsEntrySlider.Value = _cfg.GetCVar(CVars.NetPVSEntityEnterBudget);
NetPvsLeaveSlider.Value = _cfg.GetCVar(CVars.NetPVSEntityExitBudget);
NetPredictCheckbox.Pressed = _cfg.GetCVar(CVars.NetPredict);
UpdateChanges();
}
private void UpdateChanges()
{
var isEverythingSame =
NetInterpRatioSlider.Value == _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize &&
NetPredictTickBiasSlider.Value == _cfg.GetCVar(CVars.NetPredictTickBias) &&
NetPredictCheckbox.Pressed == _cfg.GetCVar(CVars.NetPredict) &&
NetPvsSpawnSlider.Value == _cfg.GetCVar(CVars.NetPVSEntityBudget) &&
NetPvsEntrySlider.Value == _cfg.GetCVar(CVars.NetPVSEntityEnterBudget) &&
NetPvsLeaveSlider.Value == _cfg.GetCVar(CVars.NetPVSEntityExitBudget);
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
NetInterpRatioLabel.Text = NetInterpRatioSlider.Value.ToString(CultureInfo.InvariantCulture);
NetPredictTickBiasLabel.Text = NetPredictTickBiasSlider.Value.ToString(CultureInfo.InvariantCulture);
NetPvsSpawnLabel.Text = NetPvsSpawnSlider.Value.ToString(CultureInfo.InvariantCulture);
NetPvsEntryLabel.Text = NetPvsEntrySlider.Value.ToString(CultureInfo.InvariantCulture);
NetPvsLeaveLabel.Text = NetPvsLeaveSlider.Value.ToString(CultureInfo.InvariantCulture);
// TODO disable / grey-out the predict and interp sliders if prediction is disabled.
// Currently no option to do this, but should be added to the slider control in general
}
}
}

View File

@@ -1,15 +1,14 @@
using System.Numerics;
using Content.Client.UserInterface.Systems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.StatusIcon.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using System.Numerics;
using Content.Shared.StatusIcon.Components;
using Content.Client.UserInterface.Systems;
using Robust.Shared.Prototypes;
using static Robust.Shared.Maths.Color;
namespace Content.Client.Overlays;
@@ -79,6 +78,10 @@ public sealed class EntityHealthBarOverlay : Overlay
continue;
}
// we are all progressing towards death every day
if (CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent) is not { } deathProgress)
continue;
var worldPosition = _transform.GetWorldPosition(xform);
var worldMatrix = Matrix3.CreateTranslation(worldPosition);
@@ -91,10 +94,6 @@ public sealed class EntityHealthBarOverlay : Overlay
var widthOfMob = bounds.Width * EyeManager.PixelsPerMeter;
var position = new Vector2(-widthOfMob / EyeManager.PixelsPerMeter / 2, yOffset / EyeManager.PixelsPerMeter);
// we are all progressing towards death every day
(float ratio, bool inCrit) deathProgress = CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent);
var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit);
// Hardcoded width of the progress bar because it doesn't match the texture.
@@ -122,10 +121,13 @@ public sealed class EntityHealthBarOverlay : Overlay
/// <summary>
/// Returns a ratio between 0 and 1, and whether the entity is in crit.
/// </summary>
private (float, bool) CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
private (float ratio, bool inCrit)? CalcProgress(EntityUid uid, MobStateComponent component, DamageableComponent dmg, MobThresholdsComponent thresholds)
{
if (_mobStateSystem.IsAlive(uid, component))
{
if (dmg.HealthBarThreshold != null && dmg.TotalDamage < dmg.HealthBarThreshold)
return null;
if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds) &&
!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out threshold, thresholds))
return (1, false);

View File

@@ -1,18 +1,20 @@
using System.Linq;
using Content.Shared.Containers;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Popups
{
@@ -29,11 +31,11 @@ namespace Content.Client.Popups
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
public IReadOnlyCollection<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
private readonly List<WorldPopupLabel> _aliveWorldLabels = new();
private readonly List<CursorPopupLabel> _aliveCursorLabels = new();
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
public const float MinimumPopupLifetime = 0.7f;
public const float MaximumPopupLifetime = 5f;
@@ -65,6 +67,15 @@ namespace Content.Client.Popups
.RemoveOverlay<PopupOverlay>();
}
private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
{
existingLabel.TotalTime = 0;
existingLabel.Repeats += 1;
existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
("popup-message", popupMessage),
("count", existingLabel.Repeats));
}
private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
{
if (message == null)
@@ -78,13 +89,20 @@ namespace Content.Client.Popups
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)));
}
var popupData = new WorldPopupData(message, type, coordinates, entity);
if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
{
WrapAndRepeatPopup(existingLabel, popupData.Message);
return;
}
var label = new WorldPopupLabel(coordinates)
{
Text = message,
Type = type,
};
_aliveWorldLabels.Add(label);
_aliveWorldLabels.Add(popupData, label);
}
#region Abstract Method Implementations
@@ -113,13 +131,20 @@ namespace Content.Client.Popups
if (recordReplay && _replayRecording.IsRecording)
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
var popupData = new CursorPopupData(message, type);
if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
{
WrapAndRepeatPopup(existingLabel, popupData.Message);
return;
}
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
{
Text = message,
Type = type,
};
_aliveCursorLabels.Add(label);
_aliveCursorLabels.Add(popupData, label);
}
public override void PopupCursor(string? message, PopupType type = PopupType.Small)
@@ -249,27 +274,37 @@ namespace Content.Client.Popups
if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
return;
for (var i = 0; i < _aliveWorldLabels.Count; i++)
if (_aliveWorldLabels.Count > 0)
{
var label = _aliveWorldLabels[i];
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
var aliveWorldToRemove = new ValueList<WorldPopupData>();
foreach (var (data, label) in _aliveWorldLabels)
{
_aliveWorldLabels.RemoveSwap(i);
i--;
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
{
aliveWorldToRemove.Add(data);
}
}
foreach (var data in aliveWorldToRemove)
{
_aliveWorldLabels.Remove(data);
}
}
for (var i = 0; i < _aliveCursorLabels.Count; i++)
if (_aliveCursorLabels.Count > 0)
{
var label = _aliveCursorLabels[i];
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label))
var aliveCursorToRemove = new ValueList<CursorPopupData>();
foreach (var (data, label) in _aliveCursorLabels)
{
_aliveCursorLabels.RemoveSwap(i);
i--;
label.TotalTime += frameTime;
if (label.TotalTime > GetPopupLifetime(label))
{
aliveCursorToRemove.Add(data);
}
}
foreach (var data in aliveCursorToRemove)
{
_aliveCursorLabels.Remove(data);
}
}
}
@@ -279,29 +314,32 @@ namespace Content.Client.Popups
public PopupType Type = PopupType.Small;
public string Text { get; set; } = string.Empty;
public float TotalTime { get; set; }
public int Repeats = 1;
}
public sealed class CursorPopupLabel : PopupLabel
{
public ScreenCoordinates InitialPos;
public CursorPopupLabel(ScreenCoordinates screenCoords)
{
InitialPos = screenCoords;
}
}
public sealed class WorldPopupLabel : PopupLabel
public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
{
/// <summary>
/// The original EntityCoordinates of the label.
/// </summary>
public EntityCoordinates InitialPos;
public WorldPopupLabel(EntityCoordinates coordinates)
{
InitialPos = coordinates;
}
public EntityCoordinates InitialPos = coordinates;
}
public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
{
public ScreenCoordinates InitialPos = screenCoords;
}
[UsedImplicitly]
private record struct WorldPopupData(
string Message,
PopupType Type,
EntityCoordinates Coordinates,
EntityUid? Entity);
[UsedImplicitly]
private record struct CursorPopupData(
string Message,
PopupType Type);
}
}

View File

@@ -1,21 +1,27 @@
using Content.Client.Power.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Wires;
namespace Content.Client.Power;
public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem
{
public override void Initialize()
{
base.Initialize();
[Dependency] private readonly SharedPopupSystem _popup = default!;
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, ActivatableUIOpenAttemptEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
protected override void OnActivate(Entity<ActivatableUIRequiresPowerComponent> ent, ref ActivatableUIOpenAttemptEvent args)
{
// Client can't predict the power properly at the moment so rely upon the server to do it.
if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
{
return;
}
if (TryComp<WiresPanelComponent>(ent.Owner, out var panel) && panel.Open)
return;
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Power.Components;
namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
}

View File

@@ -0,0 +1,23 @@
using Content.Client.Power.Components;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Client.Power.EntitySystems;
public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args)
{
if (args.Current is not ApcPowerReceiverComponentState state)
return;
component.Powered = state.Powered;
}
}

View File

@@ -0,0 +1,16 @@
using Content.Client.Power.Components;
namespace Content.Client.Power.EntitySystems;
public static class StaticPowerSystem
{
// Using this makes the call shorter.
// ReSharper disable once UnusedParameter.Global
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
{
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
return false;
return receiver.Powered;
}
}

View File

@@ -31,7 +31,7 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
_window.OpenCentered();
_window.OnNameConfirm += SendDeviceName;
_window.OnNetworkConfirm += SendSelectedNetwork;
_window.OnClose += Close;
}
private void SendSelectedNetwork(int idx)
@@ -63,7 +63,8 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
if (disposing)
{
_window!.Dispose();
_window?.Dispose();
_window = null;
}
}
}

View File

@@ -119,9 +119,4 @@ public class ActionButtonContainer : GridContainer
yield return button;
}
}
~ActionButtonContainer()
{
UserInterfaceManager.GetUIController<ActionUIController>().RemoveActionContainer();
}
}

View File

@@ -22,9 +22,4 @@ public sealed class ItemSlotButtonContainer : ItemSlotUIContainer<SlotControl>
{
_inventoryController = UserInterfaceManager.GetUIController<InventoryUIController>();
}
~ItemSlotButtonContainer()
{
_inventoryController.RemoveSlotGroup(SlotGroup);
}
}

View File

@@ -9,7 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
namespace Content.Client.UserInterface.Systems.Storage.Controls;
public sealed class ItemGridPiece : Control
public sealed class ItemGridPiece : Control, IEntityControl
{
private readonly IEntityManager _entityManager;
private readonly StorageUIController _storageController;
@@ -287,6 +287,8 @@ public sealed class ItemGridPiece : Control
var actualSize = new Vector2(boxSize.X + 1, boxSize.Y + 1);
return actualSize * new Vector2i(8, 8);
}
public EntityUid? UiEntity => Entity;
}
public enum ItemGridPieceMarks

View File

@@ -2,6 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -128,4 +131,29 @@ public sealed partial class TestPair
return list;
}
/// <summary>
/// Helper method for enabling or disabling a antag role
/// </summary>
public async Task SetAntagPref(ProtoId<AntagPrototype> id, bool value)
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(Client.User!.Value);
// what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable?
var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter;
Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
var newProfile = profile.WithAntagPreference(id, value);
await Server.WaitPost(() =>
{
prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait();
});
// And why the fuck does it always create a new preference and profile object instead of just reusing them?
var newPrefs = prefMan.GetPreferences(Client.User.Value);
var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value));
}
}

View File

@@ -65,11 +65,11 @@ public static partial class PoolManager
options.BeforeStart += () =>
{
// Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<ResettingEntitySystemTests.TestRoundRestartCleanupEvent>();
entSysMan.LoadExtraSystemType<InteractionSystemTests.TestInteractionSystem>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);

View File

@@ -0,0 +1,76 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Antag;
using Content.Server.Antag.Components;
using Content.Server.GameTicking;
using Content.Shared.GameTicking;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.IntegrationTests.Tests.GameRules;
// Once upon a time, players in the lobby weren't ever considered eligible for antag roles.
// Lets not let that happen again.
[TestFixture]
public sealed class AntagPreferenceTest
{
[Test]
public async Task TestLobbyPlayersValid()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
var server = pair.Server;
var client = pair.Client;
var ticker = server.System<GameTicker>();
var sys = server.System<AntagSelectionSystem>();
// Initially in the lobby
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
var rule = new Entity<AntagSelectionComponent>(uid, server.EntMan.GetComponent<AntagSelectionComponent>(uid));
var def = rule.Comp.Definitions.Single();
// IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
// Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
// By default, traitor/antag preferences are disabled, so the pool should be empty.
var sessions = new List<ICommonSession>{pair.Player!};
var pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(0));
// Opt into the traitor role.
await pair.SetAntagPref("Traitor", true);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(1));
pool.TryPickAndTake(pair.Server.ResolveDependency<IRobustRandom>(), out var picked);
Assert.That(picked, Is.EqualTo(pair.Player));
Assert.That(sessions.Count, Is.EqualTo(1));
// opt back out
await pair.SetAntagPref("Traitor", false);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(0));
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.CleanReturnAsync();
}
}

View File

@@ -58,6 +58,9 @@ public sealed class NukeOpsTest
Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
// Opt into the nukies role.
await pair.SetAntagPref("NukeopsCommander", true);
// There are no grids or maps
Assert.That(entMan.Count<MapComponent>(), Is.Zero);
Assert.That(entMan.Count<MapGridComponent>(), Is.Zero);
@@ -198,6 +201,7 @@ public sealed class NukeOpsTest
ticker.SetGamePreset((GamePresetPrototype?)null);
server.CfgMan.SetCVar(CCVars.GridFill, false);
await pair.SetAntagPref("NukeopsCommander", false);
await pair.CleanReturnAsync();
}
}

View File

@@ -407,7 +407,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await pair.CleanReturnAsync();
}
[Reflect(false)]
public sealed class TestInteractionSystem : EntitySystem
{
public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent;

View File

@@ -9,7 +9,6 @@ namespace Content.IntegrationTests.Tests
[TestOf(typeof(RoundRestartCleanupEvent))]
public sealed class ResettingEntitySystemTests
{
[Reflect(false)]
public sealed class TestRoundRestartCleanupEvent : EntitySystem
{
public bool HasBeenReset { get; set; }
@@ -49,8 +48,6 @@ namespace Content.IntegrationTests.Tests
system.HasBeenReset = false;
Assert.That(system.HasBeenReset, Is.False);
gameTicker.RestartRound();
Assert.That(system.HasBeenReset);

View File

@@ -135,6 +135,8 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeJobDepartment(targetId, job);
}
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
if (!newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
{
_sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag.");
@@ -168,8 +170,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
}
/// <summary>

View File

@@ -18,8 +18,6 @@ using Robust.Shared.Configuration;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using System.Linq;
namespace Content.Server.Anomaly;
@@ -69,20 +67,21 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
ChangeAnomalyStability(anomaly, Random.NextFloat(anomaly.Comp.InitialStabilityRange.Item1 , anomaly.Comp.InitialStabilityRange.Item2), anomaly.Comp);
ChangeAnomalySeverity(anomaly, Random.NextFloat(anomaly.Comp.InitialSeverityRange.Item1, anomaly.Comp.InitialSeverityRange.Item2), anomaly.Comp);
ShuffleParticlesEffect(anomaly.Comp);
ShuffleParticlesEffect(anomaly);
anomaly.Comp.Continuity = _random.NextFloat(anomaly.Comp.MinContituty, anomaly.Comp.MaxContituty);
SetBehavior(anomaly, GetRandomBehavior());
}
public void ShuffleParticlesEffect(AnomalyComponent anomaly)
public void ShuffleParticlesEffect(Entity<AnomalyComponent> anomaly)
{
var particles = new List<AnomalousParticleType>
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta, AnomalousParticleType.Sigma };
anomaly.SeverityParticleType = Random.PickAndTake(particles);
anomaly.DestabilizingParticleType = Random.PickAndTake(particles);
anomaly.WeakeningParticleType = Random.PickAndTake(particles);
anomaly.TransformationParticleType = Random.PickAndTake(particles);
anomaly.Comp.SeverityParticleType = Random.PickAndTake(particles);
anomaly.Comp.DestabilizingParticleType = Random.PickAndTake(particles);
anomaly.Comp.WeakeningParticleType = Random.PickAndTake(particles);
anomaly.Comp.TransformationParticleType = Random.PickAndTake(particles);
Dirty(anomaly);
}
private void OnShutdown(Entity<AnomalyComponent> anomaly, ref ComponentShutdown args)
@@ -198,14 +197,12 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
if (anomaly.Comp.CurrentBehavior != null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
//event broadcast
var ev = new AnomalyBehaviorChangedEvent(anomaly, anomaly.Comp.CurrentBehavior, behaviorProto);
anomaly.Comp.CurrentBehavior = behaviorProto;
RaiseLocalEvent(anomaly, ref ev, true);
var behavior = _prototype.Index(behaviorProto);
EntityManager.AddComponents(anomaly, behavior.Components);
var ev = new AnomalyBehaviorChangedEvent(anomaly, anomaly.Comp.CurrentBehavior, behaviorProto);
RaiseLocalEvent(anomaly, ref ev, true);
}
private void RemoveBehavior(Entity<AnomalyComponent> anomaly, ProtoId<AnomalyBehaviorPrototype> behaviorProto)
@@ -213,7 +210,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
if (anomaly.Comp.CurrentBehavior == null)
return;
var behavior = _prototype.Index(anomaly.Comp.CurrentBehavior.Value);
var behavior = _prototype.Index(behaviorProto);
EntityManager.RemoveComponents(anomaly, behavior.Components);
}

View File

@@ -15,26 +15,26 @@ public sealed class ShuffleParticlesAnomalySystem : EntitySystem
SubscribeLocalEvent<ShuffleParticlesAnomalyComponent, StartCollideEvent>(OnStartCollide);
}
private void OnStartCollide(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, StartCollideEvent args)
private void OnStartCollide(Entity<ShuffleParticlesAnomalyComponent> ent, ref StartCollideEvent args)
{
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
if (!TryComp<AnomalyComponent>(ent, out var anomaly))
return;
if (shuffle.ShuffleOnParticleHit && _random.Prob(shuffle.Prob))
_anomaly.ShuffleParticlesEffect(anomaly);
if (!TryComp<AnomalousParticleComponent>(args.OtherEntity, out var particle))
if (!HasComp<AnomalousParticleComponent>(args.OtherEntity))
return;
if (ent.Comp.ShuffleOnParticleHit && _random.Prob(ent.Comp.Prob))
_anomaly.ShuffleParticlesEffect((ent, anomaly));
}
private void OnPulse(EntityUid uid, ShuffleParticlesAnomalyComponent shuffle, AnomalyPulseEvent args)
private void OnPulse(Entity<ShuffleParticlesAnomalyComponent> ent, ref AnomalyPulseEvent args)
{
if (!TryComp<AnomalyComponent>(uid, out var anomaly))
if (!TryComp<AnomalyComponent>(ent, out var anomaly))
return;
if (shuffle.ShuffleOnPulse && _random.Prob(shuffle.Prob))
if (ent.Comp.ShuffleOnPulse && _random.Prob(ent.Comp.Prob))
{
_anomaly.ShuffleParticlesEffect(anomaly);
_anomaly.ShuffleParticlesEffect((ent, anomaly));
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Server.Antag.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
namespace Content.Server.Antag;
/// <summary>
/// Adds fixed objectives to an antag made with <c>AntagObjectivesComponent</c>.
/// </summary>
public sealed class AntagObjectivesSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
}
private void OnAntagSelected(Entity<AntagObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
{
Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
return;
}
foreach (var id in ent.Comp.Objectives)
{
_mind.TryAddObjective(mindId, mind, id);
}
}
}

View File

@@ -0,0 +1,52 @@
using Content.Server.Antag.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Random;
namespace Content.Server.Antag;
/// <summary>
/// Adds fixed objectives to an antag made with <c>AntagRandomObjectivesComponent</c>.
/// </summary>
public sealed class AntagRandomObjectivesSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagRandomObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
}
private void OnAntagSelected(Entity<AntagRandomObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
{
Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
return;
}
var difficulty = 0f;
foreach (var set in ent.Comp.Sets)
{
if (!_random.Prob(set.Prob))
continue;
for (var pick = 0; pick < set.MaxPicks && ent.Comp.MaxDifficulty > difficulty; pick++)
{
if (_objectives.GetRandomObjective(mindId, mind, set.Groups) is not {} objective)
continue;
_mind.AddObjective(mindId, mind, objective);
var adding = Comp<ObjectiveComponent>(objective).Difficulty;
difficulty += adding;
Log.Debug($"Added objective {ToPrettyString(objective):objective} to {ToPrettyString(args.EntityUid):player} with {adding} difficulty");
}
}
}
}

View File

@@ -27,6 +27,11 @@ public sealed partial class AntagSelectionSystem
if (mindCount >= totalTargetCount)
return false;
// TODO ANTAG fix this
// If here are two definitions with 1/10 and 10/10 slots filled, this will always return the second definition
// even though it has already met its target
// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA I fucking hate game ticker code.
// It needs to track selected minds for each definition independently.
foreach (var def in ent.Comp.Definitions)
{
var target = GetTargetAntagCount(ent, null, def);
@@ -47,12 +52,26 @@ public sealed partial class AntagSelectionSystem
/// Gets the number of antagonists that should be present for a given rule based on the provided pool.
/// A null pool will simply use the player count.
/// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool = null)
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, int? playerCount = null)
{
var count = 0;
foreach (var def in ent.Comp.Definitions)
{
count += GetTargetAntagCount(ent, pool, def);
count += GetTargetAntagCount(ent, playerCount, def);
}
return count;
}
public int GetTotalPlayerCount(IList<ICommonSession> pool)
{
var count = 0;
foreach (var session in pool)
{
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
continue;
count++;
}
return count;
@@ -62,10 +81,13 @@ public sealed partial class AntagSelectionSystem
/// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
/// A null pool will simply use the player count.
/// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, int? playerCount, AntagSelectionDefinition def)
{
var poolSize = pool?.Count ?? _playerManager.Sessions
.Count(s => s.State.Status is not SessionStatus.Disconnected and not SessionStatus.Zombie);
// TODO ANTAG
// make pool non-nullable
// Review uses and ensure that people are INTENTIONALLY including players in the lobby if this is a mid-round
// antag selection.
var poolSize = playerCount ?? GetTotalPlayerCount(_playerManager.Sessions);
// factor in other definitions' affect on the count.
var countOffset = 0;
@@ -124,7 +146,7 @@ public sealed partial class AntagSelectionSystem
}
/// <remarks>
/// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
/// Helper to get just the mind entities and not names.
/// </remarks>
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
{

View File

@@ -7,12 +7,14 @@ using Content.Server.GameTicking.Rules;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Shared.Antag;
using Content.Shared.GameTicking;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Players;
@@ -24,6 +26,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Antag;
@@ -50,6 +53,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
@@ -82,10 +87,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
continue;
if (comp.SelectionsComplete)
return;
continue;
ChooseAntags((uid, comp), pool);
comp.SelectionsComplete = true;
foreach (var session in comp.SelectedSessions)
{
@@ -103,11 +107,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
continue;
if (comp.SelectionsComplete)
continue;
ChooseAntags((uid, comp));
comp.SelectionsComplete = true;
ChooseAntags((uid, comp), args.Players);
}
}
@@ -123,12 +123,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var antag, out _))
{
// TODO ANTAG
// what why aasdiuhasdopiuasdfhksad
// stop this insanity please
// probability of antag assignment shouldn't depend on the order in which rules are returned by the query.
if (!RobustRandom.Prob(LateJoinRandomChance))
continue;
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
continue;
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
if (!TryGetNextAvailableDefinition((uid, antag), out var def))
continue;
@@ -161,57 +167,62 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
base.Started(uid, component, gameRule, args);
if (component.SelectionsComplete)
return;
// If the round has not yet started, we defer antag selection until roundstart
if (GameTicker.RunLevel != GameRunLevel.InRound)
return;
if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
if (component.SelectionsComplete)
return;
ChooseAntags((uid, component));
component.SelectionsComplete = true;
}
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
.ToList();
/// <summary>
/// Chooses antagonists from the current selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
var sessions = _playerManager.Sessions.ToList();
ChooseAntags(ent, sessions);
ChooseAntags((uid, component), players);
}
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool)
{
if (ent.Comp.SelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def);
}
ent.Comp.SelectionsComplete = true;
}
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def)
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, playerPool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
// if there is both a spawner and players getting picked, let it fall back to a spawner.
var noSpawner = def.SpawnerPrototype == null;
for (var i = 0; i < count; i++)
{
var session = (ICommonSession?) null;
if (def.PickPlayer)
{
if (!playerPool.TryPickAndTake(RobustRandom, out session))
if (!playerPool.TryPickAndTake(RobustRandom, out session) && noSpawner)
{
Log.Warning($"Couldn't pick a player for {ToPrettyString(ent):rule}, no longer choosing antags for this definition");
break;
}
if (ent.Comp.SelectedSessions.Contains(session))
if (session != null && ent.Comp.SelectedSessions.Contains(session))
{
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
continue;
}
}
MakeAntag(ent, session, def);
@@ -321,20 +332,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Gets an ordered player pool based on player preferences and the antagonist definition.
/// </summary>
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, IList<ICommonSession> sessions, AntagSelectionDefinition def)
{
var preferredList = new List<ICommonSession>();
var fallbackList = new List<ICommonSession>();
var unwantedList = new List<ICommonSession>();
var invalidList = new List<ICommonSession>();
foreach (var session in sessions)
{
if (!IsSessionValid(ent, session, def) ||
!IsEntityValid(session.AttachedEntity, def))
{
invalidList.Add(session);
continue;
}
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
@@ -345,13 +351,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
fallbackList.Add(session);
}
else
{
unwantedList.Add(session);
}
}
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList, unwantedList, invalidList });
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList });
}
/// <summary>
@@ -362,14 +364,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session == null)
return true;
mind ??= session.GetMind();
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false;
if (ent.Comp.SelectedSessions.Contains(session))
return false;
mind ??= session.GetMind();
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (mind == null)
return true;
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
switch (def.MultiAntagSetting)
@@ -398,10 +404,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </summary>
private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
public bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
{
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (entity == null)
return false;
return true;
if (HasComp<PendingClockInComponent>(entity))
return false;
@@ -423,6 +430,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return true;
}
private void OnObjectivesTextGetInfo(Entity<AntagSelectionComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
if (ent.Comp.AgentName is not {} name)
return;
args.Minds = ent.Comp.SelectedMinds;
args.AgentName = Loc.GetString(name);
}
}
/// <summary>

View File

@@ -0,0 +1,18 @@
using Content.Server.Antag;
using Content.Shared.Objectives.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Gives antags selected by this rule a fixed list of objectives.
/// </summary>
[RegisterComponent, Access(typeof(AntagObjectivesSystem))]
public sealed partial class AntagObjectivesComponent : Component
{
/// <summary>
/// List of static objectives to give.
/// </summary>
[DataField(required: true)]
public List<EntProtoId<ObjectiveComponent>> Objectives = new();
}

View File

@@ -0,0 +1,52 @@
using Content.Server.Antag;
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Gives antags selected by this rule a random list of objectives.
/// </summary>
[RegisterComponent, Access(typeof(AntagRandomObjectivesSystem))]
public sealed partial class AntagRandomObjectivesComponent : Component
{
/// <summary>
/// Each set of objectives to add.
/// </summary>
[DataField(required: true)]
public List<AntagObjectiveSet> Sets = new();
/// <summary>
/// If the total difficulty of the currently given objectives exceeds, no more will be given.
/// </summary>
[DataField(required: true)]
public float MaxDifficulty;
}
/// <summary>
/// A set of objectives to try picking.
/// Difficulty is checked over all sets, but each set has its own probability and pick count.
/// </summary>
[DataRecord]
public record struct AntagObjectiveSet()
{
/// <summary>
/// The grouping used by the objective system to pick random objectives.
/// First a group is picked from these, then an objective from that group.
/// </summary>
[DataField(required: true)]
public ProtoId<WeightedRandomPrototype> Groups = string.Empty;
/// <summary>
/// Probability of this set being used.
/// </summary>
[DataField]
public float Prob = 1f;
/// <summary>
/// Number of times to try picking objectives from this set.
/// Even if there is enough difficulty remaining, no more will be given after this.
/// </summary>
[DataField]
public int MaxPicks = 20;
}

View File

@@ -42,6 +42,13 @@ public sealed partial class AntagSelectionComponent : Component
/// Is not serialized.
/// </summary>
public HashSet<ICommonSession> SelectedSessions = new();
/// <summary>
/// Locale id for the name of the antag.
/// If this is set then the antag is listed in the round-end summary.
/// </summary>
[DataField]
public LocId? AgentName;
}
[DataDefinition]
@@ -97,6 +104,7 @@ public partial struct AntagSelectionDefinition()
/// <summary>
/// Whether or not players should be picked to inhabit this antag or not.
/// If no players are left and <see cref="SpawnerPrototype"/> is set, it will make a ghost role.
/// </summary>
[DataField]
public bool PickPlayer = true;

View File

@@ -0,0 +1,9 @@
using Content.Server.Atmos.EntitySystems;
namespace Content.Server.Atmos.Components;
/// <summary>
/// This is used for restricting anchoring pipes so that they do not overlap.
/// </summary>
[RegisterComponent, Access(typeof(PipeRestrictOverlapSystem))]
public sealed partial class PipeRestrictOverlapComponent : Component;

View File

@@ -68,7 +68,6 @@ namespace Content.Server.Atmos.EntitySystems
return;
}
ActivateAnalyzer(uid, component, args.User, args.Target);
OpenUserInterface(uid, args.User, component);
args.Handled = true;
}
@@ -86,6 +85,9 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null)
{
if (!TryOpenUserInterface(uid, user, component))
return;
component.Target = target;
component.User = user;
if (target != null)
@@ -97,7 +99,6 @@ namespace Content.Server.Atmos.EntitySystems
UpdateAppearance(uid, component);
EnsureComp<ActiveGasAnalyzerComponent>(uid);
UpdateAnalyzer(uid, component);
OpenUserInterface(uid, user, component);
}
/// <summary>
@@ -134,12 +135,12 @@ namespace Content.Server.Atmos.EntitySystems
DisableAnalyzer(uid, component);
}
private void OpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null)
private bool TryOpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
return false;
_userInterface.OpenUi(uid, GasAnalyzerUiKey.Key, user);
return _userInterface.TryOpenUi(uid, GasAnalyzerUiKey.Key, user);
}
/// <summary>

View File

@@ -0,0 +1,123 @@
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Construction.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
/// <summary>
/// This handles restricting pipe-based entities from overlapping outlets/inlets with other entities.
/// </summary>
public sealed class PipeRestrictOverlapSystem : EntitySystem
{
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _xform = default!;
private readonly List<EntityUid> _anchoredEntities = new();
private EntityQuery<NodeContainerComponent> _nodeContainerQuery;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PipeRestrictOverlapComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
SubscribeLocalEvent<PipeRestrictOverlapComponent, AnchorAttemptEvent>(OnAnchorAttempt);
_nodeContainerQuery = GetEntityQuery<NodeContainerComponent>();
}
private void OnAnchorStateChanged(Entity<PipeRestrictOverlapComponent> ent, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
return;
if (HasComp<AnchorableComponent>(ent) && CheckOverlap(ent))
{
_popup.PopupEntity(Loc.GetString("pipe-restrict-overlap-popup-blocked", ("pipe", ent.Owner)), ent);
_xform.Unanchor(ent, Transform(ent));
}
}
private void OnAnchorAttempt(Entity<PipeRestrictOverlapComponent> ent, ref AnchorAttemptEvent args)
{
if (args.Cancelled)
return;
if (!_nodeContainerQuery.TryComp(ent, out var node))
return;
var xform = Transform(ent);
if (CheckOverlap((ent, node, xform)))
{
_popup.PopupEntity(Loc.GetString("pipe-restrict-overlap-popup-blocked", ("pipe", ent.Owner)), ent, args.User);
args.Cancel();
}
}
[PublicAPI]
public bool CheckOverlap(EntityUid uid)
{
if (!_nodeContainerQuery.TryComp(uid, out var node))
return false;
return CheckOverlap((uid, node, Transform(uid)));
}
public bool CheckOverlap(Entity<NodeContainerComponent, TransformComponent> ent)
{
if (ent.Comp2.GridUid is not { } grid || !TryComp<MapGridComponent>(grid, out var gridComp))
return false;
var indices = _map.TileIndicesFor(grid, gridComp, ent.Comp2.Coordinates);
_anchoredEntities.Clear();
_map.GetAnchoredEntities((grid, gridComp), indices, _anchoredEntities);
foreach (var otherEnt in _anchoredEntities)
{
// this should never actually happen but just for safety
if (otherEnt == ent.Owner)
continue;
if (!_nodeContainerQuery.TryComp(otherEnt, out var otherComp))
continue;
if (PipeNodesOverlap(ent, (otherEnt, otherComp, Transform(otherEnt))))
return true;
}
return false;
}
public bool PipeNodesOverlap(Entity<NodeContainerComponent, TransformComponent> ent, Entity<NodeContainerComponent, TransformComponent> other)
{
var entDirs = GetAllDirections(ent).ToList();
var otherDirs = GetAllDirections(other).ToList();
foreach (var dir in entDirs)
{
foreach (var otherDir in otherDirs)
{
if ((dir & otherDir) != 0)
return true;
}
}
return false;
IEnumerable<PipeDirection> GetAllDirections(Entity<NodeContainerComponent, TransformComponent> pipe)
{
foreach (var node in pipe.Comp1.Nodes.Values)
{
// we need to rotate the pipe manually like this because the rotation doesn't update for pipes that are unanchored.
if (node is PipeNode pipeNode)
yield return pipeNode.OriginalPipeDirection.RotatePipeDirection(pipe.Comp2.LocalRotation);
}
}
}
}

View File

@@ -39,7 +39,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
var T2 = outlet.Air.Temperature;
var pressureDelta = P1 - P2;
float dt = 1/_atmosphereSystem.AtmosTickRate;
float dt = args.dt;
float dV = 0;
var denom = (T1*V2 + T2*V1);
@@ -63,7 +63,9 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
var transferMoles = n1 - (n1+n2)*T2*V1 / denom;
// Get the volume transfered to update our flow meter.
dV = n1*Atmospherics.R*T1/P1;
// When you remove x from one side and add x to the other the total difference is 2x.
// Also account for atmos speedup so that measured flow rate matches the setting on the volume pump.
dV = 2*transferMoles*Atmospherics.R*T1/P1 / _atmosphereSystem.Speedup;
// Actually transfer the gas.
_atmosphereSystem.Merge(outlet.Air, inlet.Air.Remove(transferMoles));

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("underPressureLockoutThreshold")]
public float UnderPressureLockoutThreshold = 2;
public float UnderPressureLockoutThreshold = 60; // this must be tuned in conjunction with atmos.mmos_spacing_speed
/// <summary>
/// Pressure locked vents still leak a little (leading to eventual pressurization of sealed sections)

View File

@@ -1,5 +1,7 @@
using Content.Server.Body.Systems;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
@@ -50,10 +52,16 @@ namespace Content.Server.Body.Components
public DamageSpecifier DamageRecovery = default!;
[DataField]
public TimeSpan GaspPopupCooldown = TimeSpan.FromSeconds(8);
public TimeSpan GaspEmoteCooldown = TimeSpan.FromSeconds(8);
[ViewVariables]
public TimeSpan LastGaspPopupTime;
public TimeSpan LastGaspEmoteTime;
/// <summary>
/// The emote when gasps
/// </summary>
[DataField]
public ProtoId<EmotePrototype> GaspEmote = "Gasp";
/// <summary>
/// How many cycles in a row has the mob been under-saturated?

View File

@@ -2,8 +2,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
@@ -25,9 +25,9 @@ public sealed class RespiratorSystem : EntitySystem
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly DamageableSystem _damageableSys = default!;
[Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
@@ -84,10 +84,10 @@ public sealed class RespiratorSystem : EntitySystem
if (respirator.Saturation < respirator.SuffocationThreshold)
{
if (_gameTiming.CurTime >= respirator.LastGaspPopupTime + respirator.GaspPopupCooldown)
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
{
respirator.LastGaspPopupTime = _gameTiming.CurTime;
_popupSystem.PopupEntity(Loc.GetString("lung-behavior-gasp"), uid);
respirator.LastGaspEmoteTime = _gameTiming.CurTime;
_chat.TryEmoteWithChat(uid, respirator.GaspEmote, ignoreActionBlocker: true);
}
TakeSuffocationDamage((uid, respirator));

View File

@@ -1,9 +0,0 @@
using Content.Shared.Cabinet;
namespace Content.Server.Cabinet;
public sealed class ItemCabinetSystem : SharedItemCabinetSystem
{
// shitposting on main???
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.NameIdentifier;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
@@ -23,6 +24,7 @@ public sealed partial class CargoSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
@@ -311,7 +313,7 @@ public sealed partial class CargoSystem
var temp = new HashSet<EntityUid>();
foreach (var entity in entities)
{
if (!entry.Whitelist.IsValid(entity, EntityManager))
if (!_whitelistSys.IsValid(entry.Whitelist, entity) || (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity)))
continue;
count += _stackQuery.CompOrNull(entity)?.Count ?? 1;

View File

@@ -20,6 +20,8 @@ namespace Content.Server.Cargo.Systems
{
public sealed partial class CargoSystem
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
/// </summary>
@@ -489,6 +491,9 @@ namespace Content.Server.Cargo.Systems
// Create the item itself
var item = Spawn(order.ProductId, spawn);
// Ensure the item doesn't start anchored
_transformSystem.Unanchor(item, Transform(item));
// Create a sheet of paper to write the order details on
var printed = EntityManager.SpawnEntity(paperProto, spawn);
if (TryComp<PaperComponent>(printed, out var paper))

View File

@@ -150,6 +150,14 @@ namespace Content.Server.Chat.Managers
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
}
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
{
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("message", FormattedMessage.EscapeText(message)));
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
}
public void SendAdminAlert(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);

View File

@@ -23,6 +23,7 @@ namespace Content.Server.Chat.Managers
void SendHookOOC(string sender, string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
void SendAdminAlert(string message);
void SendAdminAlert(EntityUid player, string message);

View File

@@ -35,16 +35,16 @@ public sealed class HypospraySystem : SharedHypospraySystem
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
}
private void UseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
private bool TryUseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
{
// if target is ineligible but is a container, try to draw from the container
if (!EligibleEntity(target, EntityManager, entity)
&& _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
{
TryDraw(entity, target, drawableSolution.Value, user);
return TryDraw(entity, target, drawableSolution.Value, user);
}
TryDoInject(entity, target, user);
return TryDoInject(entity, target, user);
}
private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
@@ -52,8 +52,7 @@ public sealed class HypospraySystem : SharedHypospraySystem
if (args.Handled)
return;
TryDoInject(entity, args.User, args.User);
args.Handled = true;
args.Handled = TryDoInject(entity, args.User, args.User);
}
public void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
@@ -61,8 +60,7 @@ public sealed class HypospraySystem : SharedHypospraySystem
if (args.Handled || !args.CanReach || args.Target == null)
return;
UseHypospray(entity, args.Target.Value, args.User);
args.Handled = true;
args.Handled = TryUseHypospray(entity, args.Target.Value, args.User);
}
public void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
@@ -150,12 +148,12 @@ public sealed class HypospraySystem : SharedHypospraySystem
return true;
}
private void TryDraw(Entity<HyposprayComponent> entity, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
private bool TryDraw(Entity<HyposprayComponent> entity, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
{
if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln,
out var solution) || solution.AvailableVolume == 0)
{
return;
return false;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
@@ -168,19 +166,20 @@ public sealed class HypospraySystem : SharedHypospraySystem
Loc.GetString("injector-component-target-is-empty-message",
("target", Identity.Entity(target, EntityManager))),
entity.Owner, user);
return;
return false;
}
var removedSolution = _solutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
{
return;
return false;
}
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
("amount", removedSolution.Volume),
("target", Identity.Entity(target, EntityManager))), entity.Owner, user);
return true;
}
private bool EligibleEntity(EntityUid entity, IEntityManager entMan, HyposprayComponent component)

View File

@@ -29,50 +29,43 @@ public sealed class InjectorSystem : SharedInjectorSystem
SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
}
private void UseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
{
// Handle injecting/drawing for solutions
if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
{
if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
{
TryInject(injector, target, injectableSolution.Value, user, false);
}
else if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
{
TryInject(injector, target, refillableSolution.Value, user, true);
}
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
{
TryInjectIntoBloodstream(injector, (target, bloodstream), user);
}
else
{
Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
("target", Identity.Entity(target, EntityManager))), injector, user);
}
return TryInject(injector, target, injectableSolution.Value, user, false);
if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
return TryInject(injector, target, refillableSolution.Value, user, true);
if (TryComp<BloodstreamComponent>(target, out var bloodstream))
return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
("target", Identity.Entity(target, EntityManager))), injector, user);
return false;
}
else if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
{
// Draw from a bloodstream, if the target has that
if (TryComp<BloodstreamComponent>(target, out var stream) &&
SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
{
TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
return;
return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
}
// Draw from an object (food, beaker, etc)
if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
{
TryDraw(injector, target, drawableSolution.Value, user);
}
else
{
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
}
return TryDraw(injector, target, drawableSolution.Value, user);
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
return false;
}
return false;
}
private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
@@ -80,8 +73,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
UseInjector(entity, args.Args.Target.Value, args.Args.User);
args.Handled = true;
args.Handled = TryUseInjector(entity, args.Args.Target.Value, args.Args.User);
}
private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
@@ -105,8 +97,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
return;
}
UseInjector(entity, target, args.User);
args.Handled = true;
args.Handled = TryUseInjector(entity, target, args.User);
}
/// <summary>
@@ -214,7 +205,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
});
}
private void TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
EntityUid user)
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
@@ -224,7 +215,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
Popup.PopupEntity(
Loc.GetString("injector-component-cannot-inject-message",
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
return;
return false;
}
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
@@ -233,7 +224,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
Popup.PopupEntity(
Loc.GetString("injector-component-cannot-inject-message",
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
return;
return false;
}
// Move units from attackSolution to targetSolution
@@ -249,14 +240,15 @@ public sealed class InjectorSystem : SharedInjectorSystem
Dirty(injector);
AfterInject(injector, target);
return true;
}
private void TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
private bool TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
{
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
out var solution) || solution.Volume == 0)
return;
return false;
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount =
@@ -268,7 +260,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
Loc.GetString("injector-component-target-already-full-message",
("target", Identity.Entity(targetEntity, EntityManager))),
injector.Owner, user);
return;
return false;
}
// Move units from attackSolution to targetSolution
@@ -291,6 +283,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
Dirty(injector);
AfterInject(injector, targetEntity);
return true;
}
private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
@@ -321,13 +314,13 @@ public sealed class InjectorSystem : SharedInjectorSystem
RaiseLocalEvent(target, ref ev);
}
private void TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
Entity<SolutionComponent> targetSolution, EntityUid user)
{
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
out var solution) || solution.AvailableVolume == 0)
{
return;
return false;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
@@ -340,14 +333,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
Loc.GetString("injector-component-target-is-empty-message",
("target", Identity.Entity(target, EntityManager))),
injector.Owner, user);
return;
return false;
}
// We have some snowflaked behavior for streams.
if (target.Comp != null)
{
DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
return;
return true;
}
// Move units from attackSolution to targetSolution
@@ -355,7 +348,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
{
return;
return false;
}
Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
@@ -364,6 +357,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
Dirty(injector);
AfterDraw(injector, target);
return true;
}
private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,

View File

@@ -2,6 +2,7 @@ using Content.Server.Zombies;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Content.Shared.Zombies;
namespace Content.Server.Chemistry.ReagentEffects;

View File

@@ -2,6 +2,7 @@ using Content.Server.Zombies;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Content.Shared.Zombies;
namespace Content.Server.Chemistry.ReagentEffects;

View File

@@ -1,3 +1,4 @@
using Content.Shared.Storage;
using Content.Shared.Tools;
using Robust.Shared.Prototypes;
@@ -7,18 +8,30 @@ namespace Content.Server.Construction.Components;
/// Used for something that can be refined by welder.
/// For example, glass shard can be refined to glass sheet.
/// </summary>
[RegisterComponent]
[RegisterComponent, Access(typeof(RefiningSystem))]
public sealed partial class WelderRefinableComponent : Component
{
[DataField]
public HashSet<EntProtoId>? RefineResult = new();
/// <summary>
/// The items created when the item is refined.
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> RefineResult = new();
/// <summary>
/// The amount of time it takes to refine a given item.
/// </summary>
[DataField]
public float RefineTime = 2f;
/// <summary>
/// The amount of fuel it takes to refine a given item.
/// </summary>
[DataField]
public float RefineFuel;
/// <summary>
/// The tool type needed in order to refine this item.
/// </summary>
[DataField]
public ProtoId<ToolQualityPrototype> QualityNeeded = "Welding";
}

View File

@@ -15,6 +15,7 @@ using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Storage;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -91,7 +92,14 @@ namespace Content.Server.Construction
}
// LEGACY CODE. See warning at the top of the file!
private async Task<EntityUid?> Construct(EntityUid user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
private async Task<EntityUid?> Construct(
EntityUid user,
string materialContainer,
ConstructionGraphPrototype graph,
ConstructionGraphEdge edge,
ConstructionGraphNode targetNode,
EntityCoordinates coords,
Angle angle = default)
{
// We need a place to hold our construction items!
var container = _container.EnsureContainer<Container>(user, materialContainer, out var existed);
@@ -261,7 +269,7 @@ namespace Content.Server.Construction
}
var newEntityProto = graph.Nodes[edge.Target].Entity.GetId(null, user, new(EntityManager));
var newEntity = EntityManager.SpawnEntity(newEntityProto, EntityManager.GetComponent<TransformComponent>(user).Coordinates);
var newEntity = Spawn(newEntityProto, _transformSystem.ToMapCoordinates(coords), rotation: angle);
if (!TryComp(newEntity, out ConstructionComponent? construction))
{
@@ -376,7 +384,13 @@ namespace Content.Server.Construction
}
}
if (await Construct(user, "item_construction", constructionGraph, edge, targetNode) is not { Valid: true } item)
if (await Construct(
user,
"item_construction",
constructionGraph,
edge,
targetNode,
Transform(user).Coordinates) is not { Valid: true } item)
return false;
// Just in case this is a stack, attempt to merge it. If it isn't a stack, this will just normally pick up
@@ -511,23 +525,18 @@ namespace Content.Server.Construction
return;
}
if (await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph,
edge, targetNode) is not {Valid: true} structure)
if (await Construct(user,
(ev.Ack + constructionPrototype.GetHashCode()).ToString(),
constructionGraph,
edge,
targetNode,
GetCoordinates(ev.Location),
constructionPrototype.CanRotate ? ev.Angle : Angle.Zero) is not {Valid: true} structure)
{
Cleanup();
return;
}
// We do this to be able to move the construction to its proper position in case it's anchored...
// Oh wow transform anchoring is amazing wow I love it!!!!
// ikr
var xform = Transform(structure);
var wasAnchored = xform.Anchored;
xform.Anchored = false;
xform.Coordinates = GetCoordinates(ev.Location);
xform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero;
xform.Anchored = wasAnchored;
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack, GetNetEntity(structure)));
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(user):player} has turned a {ev.PrototypeName} construction ghost into {ToPrettyString(structure)} at {Transform(structure).Coordinates}");
Cleanup();

View File

@@ -1,50 +1,51 @@
using Content.Server.Construction.Components;
using Content.Server.Stack;
using Content.Shared.Construction;
using Content.Shared.Interaction;
using Content.Shared.Stacks;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
using Content.Shared.Storage;
using Content.Shared.Tools.Systems;
using Robust.Shared.Random;
namespace Content.Server.Construction
namespace Content.Server.Construction;
public sealed class RefiningSystem : EntitySystem
{
public sealed class RefiningSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
public override void Initialize()
{
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
public override void Initialize()
base.Initialize();
SubscribeLocalEvent<WelderRefinableComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<WelderRefinableComponent, WelderRefineDoAfterEvent>(OnDoAfter);
}
private void OnInteractUsing(EntityUid uid, WelderRefinableComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
args.Handled = _toolSystem.UseTool(
args.Used,
args.User,
uid,
component.RefineTime,
component.QualityNeeded,
new WelderRefineDoAfterEvent(),
fuel: component.RefineFuel);
}
private void OnDoAfter(EntityUid uid, WelderRefinableComponent component, WelderRefineDoAfterEvent args)
{
if (args.Cancelled)
return;
var xform = Transform(uid);
var spawns = EntitySpawnCollection.GetSpawns(component.RefineResult, _random);
foreach (var spawn in spawns)
{
base.Initialize();
SubscribeLocalEvent<WelderRefinableComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<WelderRefinableComponent, WelderRefineDoAfterEvent>(OnDoAfter);
SpawnNextToOrDrop(spawn, uid, xform);
}
private void OnInteractUsing(EntityUid uid, WelderRefinableComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, component.RefineTime, component.QualityNeeded, new WelderRefineDoAfterEvent(), fuel: component.RefineFuel);
}
private void OnDoAfter(EntityUid uid, WelderRefinableComponent component, WelderRefineDoAfterEvent args)
{
if (args.Cancelled)
return;
// get last owner coordinates and delete it
var resultPosition = Transform(uid).Coordinates;
EntityManager.DeleteEntity(uid);
// spawn each result after refine
foreach (var result in component.RefineResult!)
{
var droppedEnt = Spawn(result, resultPosition);
// TODO: If something has a stack... Just use a prototype with a single thing in the stack.
// This is not a good way to do it.
if (TryComp<StackComponent>(droppedEnt, out var stack))
_stackSystem.SetCount(droppedEnt, 1, stack);
}
}
Del(uid);
}
}

View File

@@ -63,9 +63,21 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
SubscribeLocalEvent<NetworkConfiguratorComponent, NetworkConfiguratorToggleLinkMessage>(OnToggleLinks);
SubscribeLocalEvent<NetworkConfiguratorComponent, NetworkConfiguratorButtonPressedMessage>(OnConfigButtonPressed);
SubscribeLocalEvent<NetworkConfiguratorComponent, BoundUserInterfaceCheckRangeEvent>(OnUiRangeCheck);
SubscribeLocalEvent<DeviceListComponent, ComponentRemove>(OnComponentRemoved);
}
private void OnUiRangeCheck(Entity<NetworkConfiguratorComponent> ent, ref BoundUserInterfaceCheckRangeEvent args)
{
if (ent.Comp.ActiveDeviceList == null || args.Result == BoundUserInterfaceRangeResult.Fail)
return;
DebugTools.Assert(Exists(ent.Comp.ActiveDeviceList));
if (!_interactionSystem.InRangeUnobstructed(args.Actor!, ent.Comp.ActiveDeviceList.Value))
args.Result = BoundUserInterfaceRangeResult.Fail;
}
private void OnShutdown(EntityUid uid, NetworkConfiguratorComponent component, ComponentShutdown args)
{
ClearDevices(uid, component);
@@ -75,23 +87,6 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
component.ActiveDeviceList = null;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<NetworkConfiguratorComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (component.ActiveDeviceList != null
&& EntityManager.EntityExists(component.ActiveDeviceList.Value)
&& _interactionSystem.InRangeUnobstructed(uid, component.ActiveDeviceList.Value))
continue;
//The network configurator is a handheld device. There can only ever be an ui session open for the player holding the device.
_uiSystem.CloseUi(uid, NetworkConfiguratorUiKey.Configure);
}
}
private void OnMapInit(EntityUid uid, NetworkConfiguratorComponent component, MapInitEvent args)
{
UpdateListUiState(uid, component);

View File

@@ -69,7 +69,7 @@ namespace Content.Server.Doors.Systems
&& xformQuery.TryGetComponent(uid, out var xform)
&& appearanceQuery.TryGetComponent(uid, out var appearance))
{
var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery);
var (pressure, fire) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery);
_appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance);
firelock.Temperature = fire;
firelock.Pressure = pressure;

View File

@@ -1,7 +1,10 @@
using Content.Server.Administration.Managers;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
@@ -12,6 +15,8 @@ namespace Content.Server.GameTicking.Commands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public string Command => "joingame";
public string Description => "";
@@ -67,6 +72,12 @@ namespace Content.Server.GameTicking.Commands
shell.WriteLine($"{jobPrototype.LocalizedName} has no available slots.");
return;
}
if (_adminManager.IsAdmin(player) && _cfg.GetCVar(CCVars.AdminDeadminOnJoin))
{
_adminManager.DeAdmin(player);
}
ticker.MakeJoinGame(player, station, id);
return;
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Robust.Shared.Console;
@@ -8,6 +9,7 @@ namespace Content.Server.GameTicking.Commands
sealed class ObserveCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
public string Command => "observe";
public string Description => "";
@@ -28,6 +30,13 @@ namespace Content.Server.GameTicking.Commands
return;
}
var isAdminCommand = args.Length > 0 && args[0].ToLower() == "admin";
if (!isAdminCommand && _adminManager.IsAdmin(player))
{
_adminManager.DeAdmin(player);
}
if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) &&
status != PlayerGameStatus.JoinedGame)
{

View File

@@ -1,6 +1,7 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Prototypes;
@@ -42,6 +43,14 @@ public sealed partial class GameTicker
string.Empty,
"cleargamerules",
ClearGameRulesCommand);
// List game rules command.
var localizedHelp = Loc.GetString("listgamerules-command-help");
_consoleHost.RegisterCommand("listgamerules",
string.Empty,
$"listgamerules - {localizedHelp}",
ListGameRuleCommand);
}
private void ShutdownGameRules()
@@ -49,6 +58,7 @@ public sealed partial class GameTicker
_consoleHost.UnregisterCommand("addgamerule");
_consoleHost.UnregisterCommand("endgamerule");
_consoleHost.UnregisterCommand("cleargamerules");
_consoleHost.UnregisterCommand("listgamerules");
}
/// <summary>
@@ -64,6 +74,13 @@ public sealed partial class GameTicker
var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
RaiseLocalEvent(ruleEntity, ref ev, true);
var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration();
if (!HasComp<RoundstartStationVariationRuleComponent>(ruleEntity) && !HasComp<StationVariationPassRuleComponent>(ruleEntity))
{
_allPreviousGameRules.Add((currentTime, ruleId + " (Pending)"));
}
return ruleEntity;
}
@@ -110,7 +127,8 @@ public sealed partial class GameTicker
if (delayTime > TimeSpan.Zero)
{
_sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
_adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
_adminLogger.Add(LogType.EventStarted,
$"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
var delayed = EnsureComp<DelayedStartRuleComponent>(ruleEntity);
delayed.RuleStartTime = _gameTiming.CurTime + (delayTime);
@@ -118,7 +136,20 @@ public sealed partial class GameTicker
}
}
_allPreviousGameRules.Add((RoundDuration(), id));
var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration();
// Remove the first occurrence of the pending entry before adding the started entry
var pendingRuleIndex = _allPreviousGameRules.FindIndex(rule => rule.Item2 == id + " (Pending)");
if (pendingRuleIndex >= 0)
{
_allPreviousGameRules.RemoveAt(pendingRuleIndex);
}
if (!HasComp<RoundstartStationVariationRuleComponent>(ruleEntity) && !HasComp<StationVariationPassRuleComponent>(ruleEntity))
{
_allPreviousGameRules.Add((currentTime, id));
}
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
_adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}");
@@ -296,6 +327,7 @@ public sealed partial class GameTicker
if (shell.Player != null)
{
_adminLogger.Add(LogType.EventStarted, $"{shell.Player} tried to add game rule [{rule}] via command");
_chatManager.SendAdminAnnouncement(Loc.GetString("add-gamerule-admin", ("rule", rule), ("admin", shell.Player)));
}
else
{
@@ -306,6 +338,7 @@ public sealed partial class GameTicker
// Start rule if we're already in the middle of a round
if(RunLevel == GameRunLevel.InRound)
StartGameRule(ent);
}
}
@@ -349,5 +382,42 @@ public sealed partial class GameTicker
ClearGameRules();
}
[AdminCommand(AdminFlags.Admin)]
private void ListGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
{
_sawmill.Info($"{shell.Player} tried to get list of game rules via command");
_adminLogger.Add(LogType.Action, $"{shell.Player} tried to get list of game rules via command");
var message = GetGameRulesListMessage(false);
shell.WriteLine(message);
}
private string GetGameRulesListMessage(bool forChatWindow)
{
if (_allPreviousGameRules.Count > 0)
{
var sortedRules = _allPreviousGameRules.OrderBy(rule => rule.Item1).ToList();
var message = "\n";
if (!forChatWindow)
{
var header = Loc.GetString("list-gamerule-admin-header");
message += $"\n{header}\n";
message += "|------------|------------------\n";
}
foreach (var (time, rule) in sortedRules)
{
var formattedTime = time.ToString(@"hh\:mm\:ss");
message += $"| {formattedTime,-10} | {rule,-16} \n";
}
return message;
}
else
{
return Loc.GetString("list-gamerule-admin-no-rules");
}
}
#endregion
}

View File

@@ -1,4 +1,6 @@
using System.Linq;
using Content.Server.Database;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.GameWindow;
@@ -196,6 +198,15 @@ namespace Content.Server.GameTicking
_playerGameStatuses[session.UserId] = PlayerGameStatus.JoinedGame;
_db.AddRoundPlayers(RoundId, session.UserId);
if (_adminManager.HasAdminFlag(session, AdminFlags.Admin))
{
if (_allPreviousGameRules.Count > 0)
{
var rulesMessage = GetGameRulesListMessage(true);
_chatManager.SendAdminAnnouncementMessage(session, Loc.GetString("starting-rule-selected-preset", ("preset", rulesMessage)));
}
}
RaiseNetworkEvent(new TickerJoinGameEvent(), session.Channel);
}

View File

@@ -795,7 +795,7 @@ namespace Content.Server.GameTicking
}
/// <summary>
/// Event raised after players were assigned jobs by the GameTicker.
/// Event raised after players were assigned jobs by the GameTicker and have been spawned in.
/// You can give on-station people special roles by listening to this event.
/// </summary>
public sealed class RulePlayerJobsAssignedEvent

View File

@@ -0,0 +1,39 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Humanoid;
using Content.Server.Preferences.Managers;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules;
public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfileRuleComponent>
{
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagLoadProfileRuleComponent, AntagSelectEntityEvent>(OnSelectEntity);
}
private void OnSelectEntity(Entity<AntagLoadProfileRuleComponent> ent, ref AntagSelectEntityEvent args)
{
if (args.Handled)
return;
var profile = args.Session != null
? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
: HumanoidCharacterProfile.RandomWithSpecies();
if (profile?.Species is not {} speciesId || !_proto.TryIndex<SpeciesPrototype>(speciesId, out var species))
species = _proto.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
args.Entity = Spawn(species.Prototype);
_humanoid.LoadProfile(args.Entity.Value, profile);
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Makes this rules antags spawn a humanoid, either from the player's profile or a random one.
/// </summary>
[RegisterComponent]
public sealed partial class AntagLoadProfileRuleComponent : Component;

View File

@@ -8,23 +8,4 @@ namespace Content.Server.GameTicking.Rules.Components;
/// Stores data for <see cref="ThiefRuleSystem"/>.
/// </summary>
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component
{
[DataField]
public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
[DataField]
public float BigObjectiveChance = 0.7f;
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
[DataField]
public int MaxStealObjectives = 10;
}
public sealed partial class ThiefRuleComponent : Component;

View File

@@ -22,9 +22,6 @@ public sealed partial class TraitorRuleComponent : Component
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups";
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
@@ -72,7 +69,4 @@ public sealed partial class TraitorRuleComponent : Component
/// </summary>
[DataField]
public int StartingBalance = 20;
[DataField]
public int MaxDifficulty = 5;
}

View File

@@ -1,6 +1,8 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.GameTicking.Rules;
@@ -47,7 +49,8 @@ public sealed class GenericAntagRuleSystem : GameRuleSystem<GenericAntagRuleComp
private void OnObjectivesTextGetInfo(EntityUid uid, GenericAntagRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = comp.Minds;
// just temporary until this is deleted
args.Minds = comp.Minds.Select(mindId => (mindId, Comp<MindComponent>(mindId).CharacterName ?? "?")).ToList();
args.AgentName = Loc.GetString(comp.AgentName);
}
}

View File

@@ -1,11 +1,9 @@
using Content.Server.Antag;
using Content.Server.Communications;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Humanoid;
using Content.Server.Nuke;
using Content.Server.NukeOps;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Events;
@@ -13,20 +11,16 @@ using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Nuke;
using Content.Shared.NukeOps;
using Content.Shared.Preferences;
using Content.Shared.Store;
using Content.Shared.Tag;
using Content.Shared.Zombies;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
@@ -36,10 +30,7 @@ namespace Content.Server.GameTicking.Rules;
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -71,7 +62,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
}
@@ -471,24 +461,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
}
// this should really go anywhere else but im tired.
private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
{
if (args.Handled)
return;
var profile = args.Session != null
? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
: HumanoidCharacterProfile.RandomWithSpecies();
if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
{
species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
}
args.Entity = Spawn(species.Prototype);
_humanoid.LoadProfile(args.Entity.Value, profile);
}
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.TargetStation is not { } station)

View File

@@ -46,7 +46,6 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
Log.Info($"Selected {preset.ID} as the secret preset.");
_adminLogger.Add(LogType.EventStarted, $"Selected {preset.ID} as the secret preset.");
_chatManager.SendAdminAnnouncement(Loc.GetString("rule-secret-selected-preset", ("preset", preset.ID)));
foreach (var rule in preset.Rules)
{

View File

@@ -24,7 +24,6 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
@@ -33,41 +32,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
return;
//Generate objectives
GenerateObjectives(mindId, mind, ent);
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
}
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
{
// Give thieves their objectives
var difficulty = 0f;
if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
{
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup);
if (objective != null)
{
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
}
for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives
{
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup);
if (objective == null)
continue;
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
//Escape target
var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup);
if (escapeObjective != null)
_mindSystem.AddObjective(mindId, mind, escapeObjective.Value);
}
//Add mind briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
{
@@ -87,10 +54,4 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
return briefing;
}
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
args.AgentName = Loc.GetString("thief-round-end-agent-name");
}
}

View File

@@ -31,15 +31,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
public const int MaxPicks = 20;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
@@ -67,7 +64,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
@@ -112,37 +109,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
// Give traitors their objectives
if (giveObjectives)
{
var difficulty = 0f;
for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null)
continue;
_mindSystem.AddObjective(mindId, mind, objective.Value);
var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty;
difficulty += adding;
Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty");
}
}
return true;
}
private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = _antag.GetAntagMindEntityUids(uid);
args.AgentName = Loc.GetString("traitor-round-end-agent-name");
}
// TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
// TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{
var sb = new StringBuilder();

View File

@@ -1,43 +0,0 @@
using Content.Server.Lock.Components;
using Content.Server.Popups;
using Content.Shared.UserInterface;
using Content.Shared.Lock;
using Content.Server.UserInterface;
using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
namespace Content.Server.Lock.EntitySystems;
public sealed class ActivatableUIRequiresLockSystem : EntitySystem
{
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, LockToggledEvent>(LockToggled);
}
private void OnUIOpenAttempt(EntityUid uid, ActivatableUIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args)
{
if (args.Cancelled)
return;
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked != component.requireLocked)
{
args.Cancel();
if (lockComp.Locked)
_popupSystem.PopupEntity(Loc.GetString("entity-storage-component-locked-message"), uid, args.User);
}
}
private void LockToggled(EntityUid uid, ActivatableUIRequiresLockComponent component, LockToggledEvent args)
{
if (!TryComp<LockComponent>(uid, out var lockComp) || lockComp.Locked == component.requireLocked)
return;
_activatableUI.CloseAll(uid);
}
}

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.DoAfter;
using Content.Server.Humanoid;
using Content.Shared.UserInterface;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
@@ -24,7 +23,6 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MagicMirrorComponent, ActivatableUIOpenAttemptEvent>(OnOpenUIAttempt);
Subs.BuiEvents<MagicMirrorComponent>(MagicMirrorUiKey.Key,
subs =>
@@ -36,7 +34,6 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
subs.Event<MagicMirrorRemoveSlotMessage>(OnTryMagicMirrorRemoveSlot);
});
SubscribeLocalEvent<MagicMirrorComponent, AfterInteractEvent>(OnMagicMirrorInteract);
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorSelectDoAfterEvent>(OnSelectSlotDoAfter);
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorChangeColorDoAfterEvent>(OnChangeColorDoAfter);
@@ -44,23 +41,6 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorAddSlotDoAfterEvent>(OnAddSlotDoAfter);
}
private void OnMagicMirrorInteract(Entity<MagicMirrorComponent> mirror, ref AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null)
return;
if (!_uiSystem.TryOpenUi(mirror.Owner, MagicMirrorUiKey.Key, args.User))
return;
UpdateInterface(mirror.Owner, args.Target.Value, mirror.Comp);
}
private void OnOpenUIAttempt(EntityUid uid, MagicMirrorComponent mirror, ActivatableUIOpenAttemptEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.User))
args.Cancel();
}
private void OnMagicMirrorSelect(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectMessage message)
{
if (component.Target is not { } target)
@@ -83,7 +63,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
}, out var doAfterId);
},
out var doAfterId);
component.DoAfter = doAfterId;
_audio.PlayPvs(component.ChangeHairSound, uid);
@@ -137,7 +118,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
}, out var doAfterId);
},
out var doAfterId);
component.DoAfter = doAfterId;
}
@@ -189,7 +171,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
BreakOnDamage = true,
BreakOnHandChange = false,
NeedHand = true
}, out var doAfterId);
},
out var doAfterId);
component.DoAfter = doAfterId;
_audio.PlayPvs(component.ChangeHairSound, uid);
@@ -241,7 +224,8 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
BreakOnMove = true,
BreakOnHandChange = false,
NeedHand = true
}, out var doAfterId);
},
out var doAfterId);
component.DoAfter = doAfterId;
_audio.PlayPvs(component.ChangeHairSound, uid);

View File

@@ -0,0 +1,13 @@
namespace Content.Server.Movement.Components;
/// <summary>
/// Added to an entity that is ctrl-click moving their pulled object.
/// </summary>
/// <remarks>
/// This just exists so we don't have MoveEvent subs going off for every single mob constantly.
/// </remarks>
[RegisterComponent]
public sealed partial class PullMoverComponent : Component
{
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Map;
namespace Content.Server.Movement.Components;
/// <summary>
/// Added when an entity is being ctrl-click moved when pulled.
/// </summary>
[RegisterComponent]
public sealed partial class PullMovingComponent : Component
{
// Not serialized to indicate THIS CODE SUCKS, fix pullcontroller first
[ViewVariables]
public EntityCoordinates MovingTo;
}

View File

@@ -0,0 +1,318 @@
using System.Numerics;
using Content.Server.Movement.Components;
using Content.Server.Physics.Controllers;
using Content.Shared.ActionBlocker;
using Content.Shared.Gravity;
using Content.Shared.Input;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Rotatable;
using Robust.Server.Physics;
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Movement.Systems;
public sealed class PullController : VirtualController
{
/*
* This code is awful. If you try to tweak this without refactoring it I'm gonna revert it.
*/
// Parameterization for pulling:
// Speeds. Note that the speed is mass-independent (multiplied by mass).
// Instead, tuning to mass is done via the mass values below.
// Note that setting the speed too high results in overshoots (stabilized by drag, but bad)
private const float AccelModifierHigh = 15f;
private const float AccelModifierLow = 60.0f;
// High/low-mass marks. Curve is constant-lerp-constant, i.e. if you can even pull an item,
// you'll always get at least AccelModifierLow and no more than AccelModifierHigh.
private const float AccelModifierHighMass = 70.0f; // roundstart saltern emergency closet
private const float AccelModifierLowMass = 5.0f; // roundstart saltern emergency crowbar
// Used to control settling (turns off pulling).
private const float MaximumSettleVelocity = 0.1f;
private const float MaximumSettleDistance = 0.1f;
// Settle shutdown control.
// Mustn't be too massive, as that causes severe mispredicts *and can prevent it ever resolving*.
// Exists to bleed off "I pulled my crowbar" overshoots.
// Minimum velocity for shutdown to be necessary. This prevents stuff getting stuck b/c too much shutdown.
private const float SettleMinimumShutdownVelocity = 0.25f;
// Distance in which settle shutdown multiplier is at 0. It then scales upwards linearly with closer distances.
private const float SettleShutdownDistance = 1.0f;
// Velocity change of -LinearVelocity * frameTime * this
private const float SettleShutdownMultiplier = 20.0f;
// How much you must move for the puller movement check to actually hit.
private const float MinimumMovementDistance = 0.005f;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
/// <summary>
/// If distance between puller and pulled entity lower that this threshold,
/// pulled entity will not change its rotation.
/// Helps with small distance jittering
/// </summary>
private const float ThresholdRotDistance = 1;
/// <summary>
/// If difference between puller and pulled angle lower that this threshold,
/// pulled entity will not change its rotation.
/// Helps with diagonal movement jittering
/// As of further adjustments, should divide cleanly into 90 degrees
/// </summary>
private const float ThresholdRotAngle = 22.5f;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<PullableComponent> _pullableQuery;
private EntityQuery<PullerComponent> _pullerQuery;
private EntityQuery<TransformComponent> _xformQuery;
public override void Initialize()
{
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
.Register<PullingSystem>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_pullableQuery = GetEntityQuery<PullableComponent>();
_pullerQuery = GetEntityQuery<PullerComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
UpdatesAfter.Add(typeof(MoverController));
SubscribeLocalEvent<PullMovingComponent, PullStoppedMessage>(OnPullStop);
SubscribeLocalEvent<PullMoverComponent, MoveEvent>(OnPullerMove);
base.Initialize();
}
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<PullController>();
}
private void OnPullStop(Entity<PullMovingComponent> ent, ref PullStoppedMessage args)
{
RemCompDeferred<PullMovingComponent>(ent);
}
private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (session?.AttachedEntity is not { } player ||
!player.IsValid())
{
return false;
}
if (!_pullerQuery.TryComp(player, out var pullerComp))
return false;
var pulled = pullerComp.Pulling;
if (!_pullableQuery.TryComp(pulled, out var pullable))
return false;
if (_container.IsEntityInContainer(player))
return false;
// Cooldown buddy
if (_timing.CurTime < pullerComp.NextThrow)
return false;
pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown;
// Cap the distance
var range = 2f;
var fromUserCoords = coords.WithEntityId(player, EntityManager);
var userCoords = new EntityCoordinates(player, Vector2.Zero);
if (!coords.InRange(EntityManager, TransformSystem, userCoords, range))
{
var direction = fromUserCoords.Position - userCoords.Position;
// TODO: Joint API not ass
// with that being said I think throwing is the way to go but.
if (pullable.PullJointId != null &&
TryComp(player, out JointComponent? joint) &&
joint.GetJoints.TryGetValue(pullable.PullJointId, out var pullJoint) &&
pullJoint is DistanceJoint distance)
{
range = MathF.Max(0.01f, distance.MaxLength - 0.01f);
}
fromUserCoords = new EntityCoordinates(player, direction.Normalized() * (range - 0.01f));
coords = fromUserCoords.WithEntityId(coords.EntityId);
}
EnsureComp<PullMoverComponent>(player);
var moving = EnsureComp<PullMovingComponent>(pulled!.Value);
moving.MovingTo = coords;
return false;
}
private void OnPullerMove(EntityUid uid, PullMoverComponent component, ref MoveEvent args)
{
if (!_pullerQuery.TryComp(uid, out var puller))
return;
if (puller.Pulling is not { } pullable)
return;
UpdatePulledRotation(uid, pullable);
// WHY
if (args.NewPosition.EntityId == args.OldPosition.EntityId &&
(args.NewPosition.Position - args.OldPosition.Position).LengthSquared() <
MinimumMovementDistance * MinimumMovementDistance)
{
return;
}
if (_physicsQuery.TryComp(uid, out var physics))
PhysicsSystem.WakeBody(uid, body: physics);
StopMove(uid, pullable);
}
private void StopMove(Entity<PullMoverComponent?> mover, Entity<PullMovingComponent?> moving)
{
RemCompDeferred<PullMoverComponent>(mover.Owner);
RemCompDeferred<PullMovingComponent>(moving.Owner);
}
private void UpdatePulledRotation(EntityUid puller, EntityUid pulled)
{
// TODO: update once ComponentReference works with directed event bus.
if (!TryComp(pulled, out RotatableComponent? rotatable))
return;
if (!rotatable.RotateWhilePulling)
return;
var pulledXform = _xformQuery.GetComponent(pulled);
var pullerXform = _xformQuery.GetComponent(puller);
var pullerData = TransformSystem.GetWorldPositionRotation(pullerXform);
var pulledData = TransformSystem.GetWorldPositionRotation(pulledXform);
var dir = pullerData.WorldPosition - pulledData.WorldPosition;
if (dir.LengthSquared() > ThresholdRotDistance * ThresholdRotDistance)
{
var oldAngle = pulledData.WorldRotation;
var newAngle = Angle.FromWorldVec(dir);
var diff = newAngle - oldAngle;
if (Math.Abs(diff.Degrees) > ThresholdRotAngle / 2f)
{
// Ok, so this bit is difficult because ideally it would look like it's snapping to sane angles.
// Otherwise PIANO DOOR STUCK! happens.
// But it also needs to work with station rotation / align to the local parent.
// So...
var baseRotation = pulledData.WorldRotation - pulledXform.LocalRotation;
var localRotation = newAngle - baseRotation;
var localRotationSnapped = Angle.FromDegrees(Math.Floor((localRotation.Degrees / ThresholdRotAngle) + 0.5f) * ThresholdRotAngle);
TransformSystem.SetLocalRotation(pulled, localRotationSnapped, pulledXform);
}
}
}
public override void UpdateBeforeSolve(bool prediction, float frameTime)
{
base.UpdateBeforeSolve(prediction, frameTime);
var movingQuery = EntityQueryEnumerator<PullMovingComponent, PullableComponent, TransformComponent>();
while (movingQuery.MoveNext(out var pullableEnt, out var mover, out var pullable, out var pullableXform))
{
if (!mover.MovingTo.IsValid(EntityManager))
{
RemCompDeferred<PullMovingComponent>(pullableEnt);
continue;
}
if (pullable.Puller is not {Valid: true} puller)
continue;
var pullerXform = _xformQuery.Get(puller);
var pullerPosition = TransformSystem.GetMapCoordinates(pullerXform);
var movingTo = mover.MovingTo.ToMap(EntityManager, TransformSystem);
if (movingTo.MapId != pullerPosition.MapId)
{
RemCompDeferred<PullMovingComponent>(pullableEnt);
continue;
}
if (!TryComp<PhysicsComponent>(pullableEnt, out var physics) ||
physics.BodyType == BodyType.Static ||
movingTo.MapId != pullableXform.MapID)
{
RemCompDeferred<PullMovingComponent>(pullableEnt);
continue;
}
var movingPosition = movingTo.Position;
var ownerPosition = TransformSystem.GetWorldPosition(pullableXform);
var diff = movingPosition - ownerPosition;
var diffLength = diff.Length();
if (diffLength < MaximumSettleDistance && physics.LinearVelocity.Length() < MaximumSettleVelocity)
{
PhysicsSystem.SetLinearVelocity(pullableEnt, Vector2.Zero, body: physics);
RemCompDeferred<PullMovingComponent>(pullableEnt);
continue;
}
var impulseModifierLerp = Math.Min(1.0f, Math.Max(0.0f, (physics.Mass - AccelModifierLowMass) / (AccelModifierHighMass - AccelModifierLowMass)));
var impulseModifier = MathHelper.Lerp(AccelModifierLow, AccelModifierHigh, impulseModifierLerp);
var multiplier = diffLength < 1 ? impulseModifier * diffLength : impulseModifier;
// Note the implication that the real rules of physics don't apply to pulling control.
var accel = diff.Normalized() * multiplier;
// Now for the part where velocity gets shutdown...
if (diffLength < SettleShutdownDistance && physics.LinearVelocity.Length() >= SettleMinimumShutdownVelocity)
{
// Shutdown velocity increases as we get closer to centre
var scaling = (SettleShutdownDistance - diffLength) / SettleShutdownDistance;
accel -= physics.LinearVelocity * SettleShutdownMultiplier * scaling;
}
PhysicsSystem.WakeBody(pullableEnt, body: physics);
var impulse = accel * physics.Mass * frameTime;
PhysicsSystem.ApplyLinearImpulse(pullableEnt, impulse, body: physics);
// if the puller is weightless or can't move, then we apply the inverse impulse (Newton's third law).
// doing it under gravity produces an unsatisfying wiggling when pulling.
// If player can't move, assume they are on a chair and we need to prevent pull-moving.
if (_gravity.IsWeightless(puller) && pullerXform.Comp.GridUid == null || !_actionBlockerSystem.CanMove(puller))
{
PhysicsSystem.WakeBody(puller);
PhysicsSystem.ApplyLinearImpulse(puller, -impulse);
}
}
// Cleanup PullMover
var moverQuery = EntityQueryEnumerator<PullMoverComponent, PullerComponent>();
while (moverQuery.MoveNext(out var uid, out _, out var puller))
{
if (!HasComp<PullMovingComponent>(puller.Pulling))
{
RemCompDeferred<PullMoverComponent>(uid);
continue;
}
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Content.Server.NodeContainer.Nodes
/// The directions in which this pipe can connect to other pipes around it.
/// </summary>
[DataField("pipeDirection")]
private PipeDirection _originalPipeDirection;
public PipeDirection OriginalPipeDirection;
/// <summary>
/// The *current* pipe directions (accounting for rotation)
@@ -110,26 +110,26 @@ namespace Content.Server.NodeContainer.Nodes
return;
var xform = entMan.GetComponent<TransformComponent>(owner);
CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(xform.LocalRotation);
CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(xform.LocalRotation);
}
bool IRotatableNode.RotateNode(in MoveEvent ev)
{
if (_originalPipeDirection == PipeDirection.Fourway)
if (OriginalPipeDirection == PipeDirection.Fourway)
return false;
// update valid pipe direction
if (!RotationsEnabled)
{
if (CurrentPipeDirection == _originalPipeDirection)
if (CurrentPipeDirection == OriginalPipeDirection)
return false;
CurrentPipeDirection = _originalPipeDirection;
CurrentPipeDirection = OriginalPipeDirection;
return true;
}
var oldDirection = CurrentPipeDirection;
CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(ev.NewRotation);
CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(ev.NewRotation);
return oldDirection != CurrentPipeDirection;
}
@@ -142,12 +142,12 @@ namespace Content.Server.NodeContainer.Nodes
if (!RotationsEnabled)
{
CurrentPipeDirection = _originalPipeDirection;
CurrentPipeDirection = OriginalPipeDirection;
return;
}
var xform = entityManager.GetComponent<TransformComponent>(Owner);
CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(xform.LocalRotation);
CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(xform.LocalRotation);
}
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,

View File

@@ -36,14 +36,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
// go through each gamerule getting data for the roundend summary.
var summaries = new Dictionary<string, Dictionary<string, List<EntityUid>>>();
var summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>();
var query = EntityQueryEnumerator<GameRuleComponent>();
while (query.MoveNext(out var uid, out var gameRule))
{
if (!_gameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var info = new ObjectivesTextGetInfoEvent(new List<EntityUid>(), string.Empty);
var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty);
RaiseLocalEvent(uid, ref info);
if (info.Minds.Count == 0)
continue;
@@ -51,7 +51,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
// first group the gamerules by their agents, for example 2 different dragons
var agent = info.AgentName;
if (!summaries.ContainsKey(agent))
summaries[agent] = new Dictionary<string, List<EntityUid>>();
summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>();
var prepend = new ObjectivesTextPrependEvent("");
RaiseLocalEvent(uid, ref prepend);
@@ -79,7 +79,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
foreach (var (_, minds) in summary)
{
total += minds.Count;
totalInCustody += minds.Where(m => IsInCustody(m)).Count();
totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count();
}
var result = new StringBuilder();
@@ -104,19 +104,16 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
}
}
private void AddSummary(StringBuilder result, string agent, List<EntityUid> minds)
private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
{
var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();
foreach (var mindId in minds)
foreach (var (mindId, name) in minds)
{
if (!TryComp(mindId, out MindComponent? mind))
continue;
var title = GetTitle(mindId, mind);
if (title == null)
if (!TryComp<MindComponent>(mindId, out var mind))
continue;
var title = GetTitle((mindId, mind), name);
var custody = IsInCustody(mindId, mind) ? Loc.GetString("objectives-in-custody") : string.Empty;
var objectives = mind.Objectives;
@@ -238,34 +235,18 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
/// <summary>
/// Get the title for a player's mind used in round end.
/// Pass in the original entity name which is shown alongside username.
/// </summary>
public string? GetTitle(EntityUid mindId, MindComponent? mind = null)
public string GetTitle(Entity<MindComponent?> mind, string name)
{
if (!Resolve(mindId, ref mind))
return null;
var name = mind.CharacterName;
var username = (string?) null;
if (mind.OriginalOwnerUserId != null &&
_player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
if (Resolve(mind, ref mind.Comp) &&
mind.Comp.OriginalOwnerUserId != null &&
_player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData))
{
username = sessionData.UserName;
var username = sessionData.UserName;
return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
}
if (username != null)
{
if (name != null)
return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
return Loc.GetString("objectives-player-user", ("user", username));
}
// nothing to identify the player by, just give up
if (name == null)
return null;
return Loc.GetString("objectives-player-named", ("name", name));
}
}
@@ -279,7 +260,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
/// The objectives system already checks if the game rule is added so you don't need to check that in this event's handler.
/// </remarks>
[ByRefEvent]
public record struct ObjectivesTextGetInfoEvent(List<EntityUid> Minds, string AgentName);
public record struct ObjectivesTextGetInfoEvent(List<(EntityUid, string)> Minds, string AgentName);
/// <summary>
/// Raised on the game rule before text for each agent's objectives is added, letting you prepend something.

View File

@@ -1,5 +1,6 @@
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Power.Components;
namespace Content.Server.Power.Components
{
@@ -8,11 +9,8 @@ namespace Content.Server.Power.Components
/// so that it can receive power from a <see cref="IApcNet"/>.
/// </summary>
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : Component
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
[ViewVariables]
public bool Powered => (MathHelper.CloseToPercent(NetworkLoad.ReceivingPower, Load) || !NeedsPower) && !PowerDisabled;
/// <summary>
/// Amount of charge this needs from an APC per second to function.
/// </summary>
@@ -33,7 +31,7 @@ namespace Content.Server.Power.Components
{
_needsPower = value;
// Reset this so next tick will do a power update.
PoweredLastUpdate = null;
Recalculate = true;
}
}
@@ -50,7 +48,8 @@ namespace Content.Server.Power.Components
set => NetworkLoad.Enabled = !value;
}
public bool? PoweredLastUpdate;
// TODO Is this needed? It forces a PowerChangedEvent when NeedsPower is toggled even if it changes to the same state.
public bool Recalculate;
[ViewVariables]
public PowerState.Load NetworkLoad { get; } = new PowerState.Load
@@ -66,10 +65,5 @@ namespace Content.Server.Power.Components
/// Does nothing on the client.
/// </summary>
[ByRefEvent]
public readonly record struct PowerChangedEvent(bool Powered, float ReceivingPower)
{
public readonly bool Powered = Powered;
public readonly float ReceivingPower = ReceivingPower;
}
public readonly record struct PowerChangedEvent(bool Powered, float ReceivingPower);
}

View File

@@ -1,34 +1,33 @@
using Content.Shared.Popups;
using Content.Server.Power.Components;
using Content.Shared.UserInterface;
using JetBrains.Annotations;
using Content.Shared.Wires;
using Content.Server.UserInterface;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Wires;
using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
namespace Content.Server.Power.EntitySystems;
public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem
{
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, ActivatableUIOpenAttemptEvent>(OnActivate);
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
protected override void OnActivate(Entity<ActivatableUIRequiresPowerComponent> ent, ref ActivatableUIOpenAttemptEvent args)
{
if (args.Cancelled) return;
if (this.IsPowered(uid, EntityManager)) return;
if (TryComp<WiresPanelComponent>(uid, out var panel) && panel.Open)
if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
{
return;
_popup.PopupCursor(Loc.GetString("base-computer-ui-component-not-powered", ("machine", uid)), args.User);
}
if (TryComp<WiresPanelComponent>(ent.Owner, out var panel) && panel.Open)
return;
args.Cancel();
}

View File

@@ -45,7 +45,7 @@ public sealed class ApcSystem : EntitySystem
var query = EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent, UserInterfaceComponent>();
while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
{
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
{
apc.LastUiUpdate = _gameTiming.CurTime;
UpdateUIState(uid, apc, battery);

View File

@@ -19,6 +19,7 @@ namespace Content.Server.Power.EntitySystems
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly PowerNetConnectorSystem _powerNetConnector = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiver = default!;
private readonly PowerState _powerState = new();
private readonly HashSet<PowerNet> _powerNetReconnectQueue = new();
@@ -302,19 +303,27 @@ namespace Content.Server.Power.EntitySystems
var enumerator = AllEntityQuery<ApcPowerReceiverComponent>();
while (enumerator.MoveNext(out var uid, out var apcReceiver))
{
var powered = apcReceiver.Powered;
if (powered == apcReceiver.PoweredLastUpdate)
var powered = !apcReceiver.PowerDisabled
&& (!apcReceiver.NeedsPower
|| MathHelper.CloseToPercent(apcReceiver.NetworkLoad.ReceivingPower,
apcReceiver.Load));
// If new value is the same as the old, then exit
if (!apcReceiver.Recalculate && apcReceiver.Powered == powered)
continue;
if (metaQuery.GetComponent(uid).EntityPaused)
var metadata = metaQuery.Comp(uid);
if (metadata.EntityPaused)
continue;
apcReceiver.PoweredLastUpdate = powered;
var ev = new PowerChangedEvent(apcReceiver.Powered, apcReceiver.NetworkLoad.ReceivingPower);
apcReceiver.Recalculate = false;
apcReceiver.Powered = powered;
Dirty(uid, apcReceiver, metadata);
var ev = new PowerChangedEvent(powered, apcReceiver.NetworkLoad.ReceivingPower);
RaiseLocalEvent(uid, ref ev);
if (appearanceQuery.TryGetComponent(uid, out var appearance))
if (appearanceQuery.TryComp(uid, out var appearance))
_appearance.SetData(uid, PowerDeviceVisuals.Powered, powered, appearance);
}
}

View File

@@ -6,15 +6,18 @@ using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Verbs;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Server.Power.EntitySystems
{
public sealed class PowerReceiverSystem : EntitySystem
public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
@@ -38,6 +41,8 @@ namespace Content.Server.Power.EntitySystems
SubscribeLocalEvent<ApcPowerReceiverComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<PowerSwitchComponent, GetVerbsEvent<AlternativeVerb>>(AddSwitchPowerVerb);
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentGetState>(OnGetState);
_recQuery = GetEntityQuery<ApcPowerReceiverComponent>();
_provQuery = GetEntityQuery<ApcPowerProviderComponent>();
}
@@ -140,14 +145,18 @@ namespace Content.Server.Power.EntitySystems
args.Verbs.Add(verb);
}
private void OnGetState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentGetState args)
{
args.State = new ApcPowerReceiverComponentState
{
Powered = component.Powered
};
}
private void ProviderChanged(Entity<ApcPowerReceiverComponent> receiver)
{
var comp = receiver.Comp;
comp.NetworkLoad.LinkedNetwork = default;
var ev = new PowerChangedEvent(comp.Powered, comp.NetworkLoad.ReceivingPower);
RaiseLocalEvent(receiver, ref ev);
_appearance.SetData(receiver, PowerDeviceVisuals.Powered, comp.Powered);
}
/// <summary>
@@ -155,12 +164,10 @@ namespace Content.Server.Power.EntitySystems
/// Otherwise, it returns 'true' because if something doesn't take power
/// it's effectively always powered.
/// </summary>
/// <returns>True when entity has no ApcPowerReceiverComponent or is Powered. False when not.</returns>
public bool IsPowered(EntityUid uid, ApcPowerReceiverComponent? receiver = null)
{
if (!_recQuery.Resolve(uid, ref receiver, false))
return true;
return receiver.Powered;
return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered;
}
/// <summary>
@@ -192,5 +199,10 @@ namespace Content.Server.Power.EntitySystems
return !receiver.PowerDisabled; // i.e. PowerEnabled
}
public void SetLoad(ApcPowerReceiverComponent comp, float load)
{
comp.Load = load;
}
}
}

View File

@@ -20,5 +20,7 @@ namespace Content.Server.Preferences.Managers
PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
bool HavePreferencesLoaded(ICommonSession session);
Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile);
}
}

View File

@@ -29,11 +29,14 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ILogManager _log = default!;
// Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
new();
private ISawmill _sawmill = default!;
private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
public void Init()
@@ -42,6 +45,7 @@ namespace Content.Server.Preferences.Managers
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
_sawmill = _log.GetSawmill("prefs");
}
private async void HandleSelectCharacterMessage(MsgSelectCharacter message)
@@ -78,27 +82,25 @@ namespace Content.Server.Preferences.Managers
private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
{
var slot = message.Slot;
var profile = message.Profile;
var userId = message.MsgChannel.UserId;
if (profile == null)
{
Logger.WarningS("prefs",
$"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {slot}.");
return;
}
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (message.Profile == null)
_sawmill.Error($"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {message.Slot}.");
else
await SetProfile(userId, message.Slot, message.Profile);
}
public async Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile)
{
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
{
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
_sawmill.Error($"Tried to modify user {userId} preferences before they loaded.");
return;
}
if (slot < 0 || slot >= MaxCharacterSlots)
{
return;
}
var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId);
@@ -112,10 +114,8 @@ namespace Content.Server.Preferences.Managers
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
await _db.SaveCharacterSlotAsync(message.MsgChannel.UserId, message.Profile, message.Slot);
}
if (ShouldStorePrefs(session.Channel.AuthType))
await _db.SaveCharacterSlotAsync(userId, profile, slot);
}
private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)

View File

@@ -50,6 +50,9 @@ public sealed partial class FTLComponent : Component
Params = AudioParams.Default.WithVolume(-3f).WithLoop(true)
};
[DataField]
public EntityUid? StartupStream;
[DataField]
public EntityUid? TravelStream;
}

View File

@@ -22,6 +22,7 @@ using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.GameTicking;
using Content.Shared.Localizations;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Events;
using Content.Shared.Tag;
@@ -287,7 +288,9 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
if (TryComp(targetGrid.Value, out TransformComponent? targetXform))
{
var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery);
_chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), ("direction", angle.GetDir())), playDefaultSound: false);
var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
var location = FormattedMessage.RemoveMarkup(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform)));
_chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false);
}
// shuttle timers
@@ -313,8 +316,13 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
}
else
{
var location = FormattedMessage.RemoveMarkup(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform)));
_chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-nearby", ("direction", location)), playDefaultSound: false);
if (TryComp<TransformComponent>(targetGrid.Value, out var targetXform))
{
var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery);
var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
var location = FormattedMessage.RemoveMarkup(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform)));
_chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-nearby", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false);
}
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}");
// TODO: Need filter extensions or something don't blame me.

View File

@@ -24,6 +24,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using FTLMapComponent = Content.Shared.Shuttles.Components.FTLMapComponent;
@@ -343,12 +344,8 @@ public sealed partial class ShuttleSystem
component = AddComp<FTLComponent>(uid);
component.State = FTLState.Starting;
var audio = _audio.PlayPvs(_startupSound, uid);
audio.Value.Component.Flags |= AudioFlags.GridAudio;
if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
{
_transform.SetLocalPosition(audio.Value.Entity, gridPhysics.LocalCenter);
}
_audio.SetGridAudio(audio);
component.StartupStream = audio?.Entity;
// TODO: Play previs here for docking arrival.
@@ -377,6 +374,17 @@ public sealed partial class ShuttleSystem
var body = _physicsQuery.GetComponent(entity);
var shuttleCenter = body.LocalCenter;
// Leave audio at the old spot
// Just so we don't clip
if (fromMapUid != null && TryComp(comp.StartupStream, out AudioComponent? startupAudio))
{
var clippedAudio = _audio.PlayStatic(_startupSound, Filter.Broadcast(),
new EntityCoordinates(fromMapUid.Value, _maps.GetGridPosition(entity.Owner)), true, startupAudio.Params);
_audio.SetPlaybackPosition(clippedAudio, entity.Comp1.StartupTime);
clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
}
// Offset the start by buffer range just to avoid overlap.
var ftlStart = new EntityCoordinates(ftlMap, new Vector2(_index + width / 2f, 0f) - shuttleCenter);
@@ -402,15 +410,7 @@ public sealed partial class ShuttleSystem
// Audio
var wowdio = _audio.PlayPvs(comp.TravelSound, uid);
comp.TravelStream = wowdio?.Entity;
if (wowdio?.Component != null)
{
wowdio.Value.Component.Flags |= AudioFlags.GridAudio;
if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
{
_transform.SetLocalPosition(wowdio.Value.Entity, gridPhysics.LocalCenter);
}
}
_audio.SetGridAudio(wowdio);
}
/// <summary>
@@ -509,13 +509,7 @@ public sealed partial class ShuttleSystem
comp.TravelStream = _audio.Stop(comp.TravelStream);
var audio = _audio.PlayPvs(_arrivalSound, uid);
audio.Value.Component.Flags |= AudioFlags.GridAudio;
// TODO: Shitcode til engine fix
if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
{
_transform.SetLocalPosition(audio.Value.Entity, gridPhysics.LocalCenter);
}
_audio.SetGridAudio(audio);
if (TryComp<FTLDestinationComponent>(uid, out var dest))
{

View File

@@ -41,6 +41,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;

View File

@@ -5,7 +5,6 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.UserInterface;
using Content.Shared.Access.Systems;
using Content.Shared.Alert;
using Content.Shared.Database;
@@ -70,7 +69,6 @@ public sealed partial class BorgSystem : SharedBorgSystem
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
@@ -214,13 +212,6 @@ public sealed partial class BorgSystem : SharedBorgSystem
UpdateUI(uid, component);
}
private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args)
{
// borgs can't view their own ui
if (args.User == uid)
args.Cancel();
}
private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args)
{
args.Dead = true;

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Ensnaring;
using Content.Shared.CombatMode;
using Content.Shared.Cuffs;
using Content.Shared.Cuffs.Components;
using Content.Shared.Database;
@@ -10,7 +9,6 @@ using Content.Shared.Ensnaring.Components;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
@@ -18,7 +16,6 @@ using Content.Shared.Popups;
using Content.Shared.Strip;
using Content.Shared.Strip.Components;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -28,7 +25,6 @@ namespace Content.Server.Strip
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly EnsnareableSystem _ensnaringSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedCuffableSystem _cuffableSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
@@ -45,7 +41,6 @@ namespace Content.Server.Strip
SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<Verb>>(AddStripVerb);
SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<ExamineVerb>>(AddStripExamineVerb);
SubscribeLocalEvent<StrippableComponent, ActivateInWorldEvent>(OnActivateInWorld);
// BUI
SubscribeLocalEvent<StrippableComponent, StrippingSlotButtonPressed>(OnStripButtonPressed);
@@ -68,7 +63,7 @@ namespace Content.Server.Strip
{
Text = Loc.GetString("strip-verb-get-data-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Act = () => StartOpeningStripper(args.User, (uid, component), true),
Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
};
args.Verbs.Add(verb);
@@ -86,37 +81,13 @@ namespace Content.Server.Strip
{
Text = Loc.GetString("strip-verb-get-data-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Act = () => StartOpeningStripper(args.User, (uid, component), true),
Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
Category = VerbCategory.Examine,
};
args.Verbs.Add(verb);
}
private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args)
{
if (args.Target == args.User)
return;
if (!HasComp<ActorComponent>(args.User))
return;
StartOpeningStripper(args.User, (uid, component));
}
public override void StartOpeningStripper(EntityUid user, Entity<StrippableComponent> strippable, bool openInCombat = false)
{
base.StartOpeningStripper(user, strippable, openInCombat);
if (TryComp<CombatModeComponent>(user, out var mode) && mode.IsInCombatMode && !openInCombat)
return;
if (HasComp<StrippingComponent>(user))
{
_userInterfaceSystem.OpenUi(strippable.Owner, StrippingUiKey.Key, user);
}
}
private void OnStripButtonPressed(Entity<StrippableComponent> strippable, ref StrippingSlotButtonPressed args)
{
if (args.Actor is not { Valid: true } user ||
@@ -442,6 +413,9 @@ namespace Content.Server.Strip
var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
if (!stealth)
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
var prefix = stealth ? "stealthily " : "";
_adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");

View File

@@ -19,6 +19,7 @@ using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Content.Shared.VendingMachines;
using Content.Shared.Wall;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -39,6 +40,8 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
private const float WallVendEjectDistanceFromWall = 1f;
public override void Initialize()
{
base.Initialize();
@@ -384,7 +387,20 @@ namespace Content.Server.VendingMachines
return;
}
var ent = Spawn(vendComponent.NextItemToEject, Transform(uid).Coordinates);
// Default spawn coordinates
var spawnCoordinates = Transform(uid).Coordinates;
//Make sure the wallvends spawn outside of the wall.
if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
{
var offset = wallMountComponent.Direction.ToWorldVec() * WallVendEjectDistanceFromWall;
spawnCoordinates = spawnCoordinates.Offset(offset);
}
var ent = Spawn(vendComponent.NextItemToEject, spawnCoordinates);
if (vendComponent.ThrowNextItem)
{
var range = vendComponent.NonLimitedEjectRange;

View File

@@ -4,27 +4,23 @@ using System.Threading;
using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.DoAfter;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Tools.Components;
using Content.Shared.UserInterface;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
namespace Content.Server.Wires;
public sealed class WiresSystem : SharedWiresSystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
@@ -52,8 +48,6 @@ public sealed class WiresSystem : SharedWiresSystem
SubscribeLocalEvent<WiresComponent, TimedWireEvent>(OnTimedWire);
SubscribeLocalEvent<WiresComponent, PowerChangedEvent>(OnWiresPowered);
SubscribeLocalEvent<WiresComponent, WireDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<ActivatableUIRequiresPanelComponent, ActivatableUIOpenAttemptEvent>(OnAttemptOpenActivatableUI);
SubscribeLocalEvent<ActivatableUIRequiresPanelComponent, PanelChangedEvent>(OnActivatableUIPanelChanged);
SubscribeLocalEvent<WiresPanelSecurityComponent, WiresPanelSecurityEvent>(SetWiresPanelSecurity);
}
@@ -473,23 +467,6 @@ public sealed class WiresSystem : SharedWiresSystem
_uiSystem.CloseUi(ent.Owner, WiresUiKey.Key);
}
private void OnAttemptOpenActivatableUI(EntityUid uid, ActivatableUIRequiresPanelComponent component, ActivatableUIOpenAttemptEvent args)
{
if (args.Cancelled || !TryComp<WiresPanelComponent>(uid, out var wires))
return;
if (component.RequireOpen != wires.Open)
args.Cancel();
}
private void OnActivatableUIPanelChanged(EntityUid uid, ActivatableUIRequiresPanelComponent component, ref PanelChangedEvent args)
{
if (args.Open == component.RequireOpen)
return;
_activatableUI.CloseAll(uid);
}
private void OnMapInit(EntityUid uid, WiresComponent component, MapInitEvent args)
{
if (!string.IsNullOrEmpty(component.LayoutId))

View File

@@ -152,25 +152,25 @@ public sealed partial class AnomalyComponent : Component
/// <summary>
/// The particle type that increases the severity of the anomaly.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public AnomalousParticleType SeverityParticleType;
/// <summary>
/// The particle type that destabilizes the anomaly.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public AnomalousParticleType DestabilizingParticleType;
/// <summary>
/// The particle type that weakens the anomalys health.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public AnomalousParticleType WeakeningParticleType;
/// <summary>
/// The particle type that change anomaly behaviour.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public AnomalousParticleType TransformationParticleType;
#region Points and Vessels
@@ -317,6 +317,7 @@ public readonly record struct AnomalyHealthChangedEvent(EntityUid Anomaly, float
/// <summary>
/// Event broadcast when an anomaly's behavior is changed.
/// This is raised after the relevant components are applied
/// </summary>
[ByRefEvent]
public readonly record struct AnomalyBehaviorChangedEvent(EntityUid Anomaly, ProtoId<AnomalyBehaviorPrototype>? Old, ProtoId<AnomalyBehaviorPrototype>? New);

View File

@@ -22,6 +22,14 @@ public enum AntagAcceptability
public enum AntagSelectionTime : byte
{
/// <summary>
/// Antag roles are assigned before players are assigned jobs and spawned in.
/// This prevents antag selection from happening if the round is on-going.
/// </summary>
PrePlayerSpawn,
/// <summary>
/// Antag roles get assigned after players have been assigned jobs and have spawned in.
/// </summary>
PostPlayerSpawn
}

View File

@@ -1050,7 +1050,7 @@ namespace Content.Shared.CCVar
/// 1.0 for instant spacing, 0.2 means 20% of remaining air lost each time
/// </summary>
public static readonly CVarDef<float> AtmosSpacingEscapeRatio =
CVarDef.Create("atmos.mmos_spacing_speed", 0.05f, CVar.SERVERONLY);
CVarDef.Create("atmos.mmos_spacing_speed", 0.15f, CVar.SERVERONLY);
/// <summary>
/// Minimum amount of air allowed on a spaced tile before it is reset to 0 immediately in kPa
@@ -1254,7 +1254,7 @@ namespace Content.Shared.CCVar
/// Config for when the restart vote should be allowed to be called based on percentage of ghosts.
///
public static readonly CVarDef<int> VoteRestartGhostPercentage =
CVarDef.Create("vote.restart_ghost_percentage", 75, CVar.SERVERONLY);
CVarDef.Create("vote.restart_ghost_percentage", 55, CVar.SERVERONLY);
/// <summary>
/// See vote.enabled, but specific to preset votes

View File

@@ -1,43 +1,25 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Cabinet;
/// <summary>
/// Used for entities that can be opened, closed, and can hold one item. E.g., fire extinguisher cabinets.
/// Used for entities that can be opened, closed, and can hold one item. E.g., fire extinguisher cabinets.
/// Requires <c>OpenableComponent</c>.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[RegisterComponent, NetworkedComponent, Access(typeof(ItemCabinetSystem))]
public sealed partial class ItemCabinetComponent : Component
{
/// <summary>
/// Sound to be played when the cabinet door is opened.
/// Name of the <see cref="ItemSlot"/> that stores the actual item.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? DoorSound;
/// <summary>
/// The <see cref="ItemSlot"/> that stores the actual item. The entity whitelist, sounds, and other
/// behaviours are specified by this <see cref="ItemSlot"/> definition.
/// </summary>
[DataField, ViewVariables]
public ItemSlot CabinetSlot = new();
/// <summary>
/// Whether the cabinet is currently open or not.
/// </summary>
[DataField, AutoNetworkedField]
public bool Opened;
/// <summary>
/// The state for when the cabinet is open
/// </summary>
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public string? OpenState;
/// <summary>
/// The state for when the cabinet is closed
/// </summary>
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public string? ClosedState;
[DataField]
public string Slot = "ItemCabinet";
}
[Serializable, NetSerializable]
public enum ItemCabinetVisuals : byte
{
ContainsItem,
Layer
}

View File

@@ -0,0 +1,95 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Containers;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Cabinet;
/// <summary>
/// Controls ItemCabinet slot locking and visuals.
/// </summary>
public sealed class ItemCabinetSystem : EntitySystem
{
[Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemCabinetComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ItemCabinetComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ItemCabinetComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ItemCabinetComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ItemCabinetComponent, OpenableOpenedEvent>(OnOpened);
SubscribeLocalEvent<ItemCabinetComponent, OpenableClosedEvent>(OnClosed);
}
private void OnStartup(Entity<ItemCabinetComponent> ent, ref ComponentStartup args)
{
UpdateAppearance(ent);
}
private void OnMapInit(Entity<ItemCabinetComponent> ent, ref MapInitEvent args)
{
// update at mapinit to avoid copy pasting locked: true and locked: false for each closed/open prototype
SetSlotLock(ent, !_openable.IsOpen(ent));
}
private void UpdateAppearance(Entity<ItemCabinetComponent> ent)
{
_appearance.SetData(ent, ItemCabinetVisuals.ContainsItem, HasItem(ent));
}
private void OnContainerModified(EntityUid uid, ItemCabinetComponent component, ContainerModifiedMessage args)
{
if (args.Container.ID == component.Slot)
UpdateAppearance((uid, component));
}
private void OnOpened(Entity<ItemCabinetComponent> ent, ref OpenableOpenedEvent args)
{
SetSlotLock(ent, false);
}
private void OnClosed(Entity<ItemCabinetComponent> ent, ref OpenableClosedEvent args)
{
SetSlotLock(ent, true);
}
/// <summary>
/// Tries to get the cabinet's item slot.
/// </summary>
public bool TryGetSlot(Entity<ItemCabinetComponent> ent, [NotNullWhen(true)] out ItemSlot? slot)
{
slot = null;
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return false;
return _slots.TryGetSlot(ent, ent.Comp.Slot, out slot, slots);
}
/// <summary>
/// Returns true if the cabinet contains an item.
/// </summary>
public bool HasItem(Entity<ItemCabinetComponent> ent)
{
return TryGetSlot(ent, out var slot) && slot.HasItem;
}
/// <summary>
/// Lock or unlock the underlying item slot.
/// </summary>
public void SetSlotLock(Entity<ItemCabinetComponent> ent, bool closed)
{
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return;
if (_slots.TryGetSlot(ent, ent.Comp.Slot, out var slot, slots))
_slots.SetLock(ent, slot, closed, slots);
}
}

View File

@@ -1,136 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction;
using Content.Shared.Lock;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Cabinet;
public abstract class SharedItemCabinetSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ItemCabinetComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<ItemCabinetComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<ItemCabinetComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<ItemCabinetComponent, AfterAutoHandleStateEvent>(OnComponentHandleState);
SubscribeLocalEvent<ItemCabinetComponent, ActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<ItemCabinetComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleOpenVerb);
SubscribeLocalEvent<ItemCabinetComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ItemCabinetComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ItemCabinetComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
}
private void OnComponentInit(EntityUid uid, ItemCabinetComponent cabinet, ComponentInit args)
{
_itemSlots.AddItemSlot(uid, "ItemCabinet", cabinet.CabinetSlot);
}
private void OnComponentRemove(EntityUid uid, ItemCabinetComponent cabinet, ComponentRemove args)
{
_itemSlots.RemoveItemSlot(uid, cabinet.CabinetSlot);
}
private void OnComponentStartup(EntityUid uid, ItemCabinetComponent cabinet, ComponentStartup args)
{
UpdateAppearance(uid, cabinet);
_itemSlots.SetLock(uid, cabinet.CabinetSlot, !cabinet.Opened);
}
private void OnComponentHandleState(Entity<ItemCabinetComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateAppearance(ent, ent);
}
protected virtual void UpdateAppearance(EntityUid uid, ItemCabinetComponent? cabinet = null)
{
// we don't fuck with appearance data, and instead just manually update the sprite on the client
}
private void OnContainerModified(EntityUid uid, ItemCabinetComponent cabinet, ContainerModifiedMessage args)
{
if (!cabinet.Initialized)
return;
if (args.Container.ID == cabinet.CabinetSlot.ID)
UpdateAppearance(uid, cabinet);
}
private void OnLockToggleAttempt(EntityUid uid, ItemCabinetComponent cabinet, ref LockToggleAttemptEvent args)
{
// Cannot lock or unlock while open.
if (cabinet.Opened)
args.Cancelled = true;
}
private void AddToggleOpenVerb(EntityUid uid, ItemCabinetComponent cabinet, GetVerbsEvent<AlternativeVerb> args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
return;
if (TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
return;
// Toggle open verb
AlternativeVerb toggleVerb = new()
{
Act = () => ToggleItemCabinet(uid, args.User, cabinet)
};
if (cabinet.Opened)
{
toggleVerb.Text = Loc.GetString("verb-common-close");
toggleVerb.Icon =
new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/close.svg.192dpi.png"));
}
else
{
toggleVerb.Text = Loc.GetString("verb-common-open");
toggleVerb.Icon =
new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/open.svg.192dpi.png"));
}
args.Verbs.Add(toggleVerb);
}
private void OnActivateInWorld(EntityUid uid, ItemCabinetComponent comp, ActivateInWorldEvent args)
{
if (args.Handled)
return;
args.Handled = true;
ToggleItemCabinet(uid, args.User, comp);
}
/// <summary>
/// Toggles the ItemCabinet's state.
/// </summary>
public void ToggleItemCabinet(EntityUid uid, EntityUid? user = null, ItemCabinetComponent? cabinet = null)
{
if (!Resolve(uid, ref cabinet))
return;
if (TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
return;
cabinet.Opened = !cabinet.Opened;
Dirty(uid, cabinet);
_itemSlots.SetLock(uid, cabinet.CabinetSlot, !cabinet.Opened);
if (_timing.IsFirstTimePredicted)
{
UpdateAppearance(uid, cabinet);
_audio.PlayPredicted(cabinet.DoorSound, uid, user, AudioParams.Default.WithVariation(0.15f));
}
}
}

View File

@@ -31,7 +31,7 @@ public sealed partial class CargoBountyPrototype : IPrototype
/// <summary>
/// The entries that must be satisfied for the cargo bounty to be complete.
/// </summary>
[DataField( required: true)]
[DataField(required: true)]
public List<CargoBountyItemEntry> Entries = new();
/// <summary>
@@ -50,6 +50,12 @@ public readonly partial record struct CargoBountyItemEntry()
[DataField(required: true)]
public EntityWhitelist Whitelist { get; init; } = default!;
/// <summary>
/// A blacklist that can be used to exclude items in the whitelist.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist { get; init; } = null;
// todo: implement some kind of simple generic condition system
/// <summary>

View File

@@ -5,8 +5,6 @@ using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Damage
{
@@ -18,7 +16,7 @@ namespace Content.Shared.Damage
/// may also have resistances to certain damage types, defined via a <see cref="DamageModifierSetPrototype"/>.
/// </remarks>
[RegisterComponent]
[NetworkedComponent()]
[NetworkedComponent]
[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
public sealed partial class DamageableComponent : Component
{
@@ -26,8 +24,8 @@ namespace Content.Shared.Damage
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
/// If null, all damage types will be supported.
/// </summary>
[DataField("damageContainer", customTypeSerializer: typeof(PrototypeIdSerializer<DamageContainerPrototype>))]
public string? DamageContainerID;
[DataField("damageContainer")]
public ProtoId<DamageContainerPrototype>? DamageContainerID;
/// <summary>
/// This <see cref="DamageModifierSetPrototype"/> will be applied to any damage that is dealt to this container,
@@ -37,8 +35,8 @@ namespace Content.Shared.Damage
/// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
/// to reduce duplication.
/// </remarks>
[DataField("damageModifierSet", customTypeSerializer: typeof(PrototypeIdSerializer<DamageModifierSetPrototype>))]
public string? DamageModifierSetId;
[DataField("damageModifierSet")]
public ProtoId<DamageModifierSetPrototype>? DamageModifierSetId;
/// <summary>
/// All the damage information is stored in this <see cref="DamageSpecifier"/>.
@@ -46,7 +44,7 @@ namespace Content.Shared.Damage
/// <remarks>
/// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
/// </remarks>
[DataField("damage", readOnly: true)] //todo remove this readonly when implementing writing to damagespecifier
[DataField(readOnly: true)] //todo remove this readonly when implementing writing to damagespecifier
public DamageSpecifier Damage = new();
/// <summary>
@@ -64,8 +62,8 @@ namespace Content.Shared.Damage
[ViewVariables]
public FixedPoint2 TotalDamage;
[DataField("radiationDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> RadiationDamageTypeIDs = new() { "Radiation" };
[DataField("radiationDamageTypes")]
public List<ProtoId<DamageTypePrototype>> RadiationDamageTypeIDs = new() { "Radiation" };
[DataField]
public Dictionary<MobState, ProtoId<StatusIconPrototype>> HealthIcons = new()
@@ -77,6 +75,9 @@ namespace Content.Shared.Damage
[DataField]
public ProtoId<StatusIconPrototype> RottingIcon = "HealthIconRotting";
[DataField]
public FixedPoint2? HealthBarThreshold;
}
[Serializable, NetSerializable]
@@ -84,13 +85,16 @@ namespace Content.Shared.Damage
{
public readonly Dictionary<string, FixedPoint2> DamageDict;
public readonly string? ModifierSetId;
public readonly FixedPoint2? HealthBarThreshold;
public DamageableComponentState(
Dictionary<string, FixedPoint2> damageDict,
string? modifierSetId)
string? modifierSetId,
FixedPoint2? healthBarThreshold)
{
DamageDict = damageDict;
ModifierSetId = modifierSetId;
HealthBarThreshold = healthBarThreshold;
}
}
}

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