Merge branch 'master' into DONOTMAPTEST

This commit is contained in:
Killerqu00
2025-02-04 12:56:47 +01:00
763 changed files with 264223 additions and 8905 deletions

View File

@@ -46,7 +46,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog" };
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;

View File

@@ -39,6 +39,6 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
if (message is not CargoBountyConsoleState state)
return;
_menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
_menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip);
}
}

View File

@@ -0,0 +1,22 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="10 10 10 0"
HorizontalExpand="True">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<RichTextLabel Name="RewardLabel"/>
<RichTextLabel Name="ManifestLabel"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" MinWidth="120" Margin="0 0 10 0">
<RichTextLabel Name="TimestampLabel" HorizontalAlignment="Right" />
<RichTextLabel Name="IdLabel" HorizontalAlignment="Right" />
</BoxContainer>
</BoxContainer>
<customControls:HSeparator Margin="5 10 5 10"/>
<RichTextLabel Name="NoticeLabel" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -0,0 +1,49 @@
using Content.Client.Message;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class BountyHistoryEntry : BoxContainer
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public BountyHistoryEntry(CargoBountyHistoryData bounty)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
return;
var items = new List<string>();
foreach (var entry in bountyPrototype.Entries)
{
items.Add(Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name))));
}
ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id)));
TimestampLabel.SetMarkup(bounty.Timestamp.ToString(@"hh\:mm\:ss"));
if (bounty.Result == CargoBountyHistoryData.BountyResult.Completed)
{
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-completed-label"));
}
else
{
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-skipped-label",
("id", bounty.ActorName ?? "")));
}
}
}

View File

@@ -11,15 +11,28 @@
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True">
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="BountyEntriesContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
</BoxContainer>
HorizontalExpand="True" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<Label Name="NoHistoryLabel"
Text="{Loc 'bounty-console-history-empty-label'}"
Visible="False"
Align="Center" />
<BoxContainer Name="BountyHistoryContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
</TabContainer>
</PanelContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">

View File

@@ -15,9 +15,12 @@ public sealed partial class CargoBountyMenu : FancyWindow
public CargoBountyMenu()
{
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle(0, Loc.GetString("bounty-console-tab-available-label"));
MasterTabContainer.SetTabTitle(1, Loc.GetString("bounty-console-tab-history-label"));
}
public void UpdateEntries(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
public void UpdateEntries(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
{
BountyEntriesContainer.Children.Clear();
foreach (var b in bounties)
@@ -32,5 +35,21 @@ public sealed partial class CargoBountyMenu : FancyWindow
{
MinHeight = 10
});
BountyHistoryContainer.Children.Clear();
if (history.Count == 0)
{
NoHistoryLabel.Visible = true;
}
else
{
NoHistoryLabel.Visible = false;
// Show the history in reverse, so last entry is first in the list
for (var i = history.Count - 1; i >= 0; i--)
{
BountyHistoryContainer.AddChild(new BountyHistoryEntry(history[i]));
}
}
}
}

View File

@@ -39,6 +39,8 @@ public sealed class CriminalRecordsConsoleBoundUserInterface : BoundUserInterfac
SendMessage(new CriminalRecordChangeStatus(status, null));
_window.OnDialogConfirmed += (status, reason) =>
SendMessage(new CriminalRecordChangeStatus(status, reason));
_window.OnStatusFilterPressed += (statusFilter) =>
SendMessage(new CriminalRecordSetStatusFilter(statusFilter));
_window.OnHistoryUpdated += UpdateHistory;
_window.OnHistoryClosed += () => _historyWindow?.Close();
_window.OnClose += Close;

View File

@@ -1,36 +1,142 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'criminal-records-console-window-title'}"
MinSize="660 400">
MinSize="695 440">
<BoxContainer Orientation="Vertical">
<!-- Record search bar
TODO: make this into a control shared with general records -->
<BoxContainer Margin="5 5 5 10" HorizontalExpand="true" VerticalAlignment="Center">
<OptionButton Name="FilterType" MinWidth="200" Margin="0 0 10 0"/> <!-- Populated in constructor -->
<LineEdit Name="FilterText" PlaceHolder="{Loc 'criminal-records-filter-placeholder'}" HorizontalExpand="True"/>
<BoxContainer Name="AllList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="8">
<!-- Record search bar -->
<BoxContainer Margin="5 5 5 10"
HorizontalExpand="true"
VerticalAlignment="Center">
<OptionButton Name="FilterType"
MinWidth="250"
Margin="0 0 10 0" />
<!-- Populated in constructor -->
<LineEdit Name="FilterText"
PlaceHolder="{Loc 'criminal-records-filter-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer Orientation="Horizontal"
VerticalExpand="True">
<!-- Record listing -->
<BoxContainer Orientation="Vertical" Margin="5" MinWidth="250" MaxWidth="250">
<Label Name="RecordListingTitle" Text="{Loc 'criminal-records-console-records-list-title'}" HorizontalExpand="True" Align="Center"/>
<Label Name="NoRecords" Text="{Loc 'criminal-records-console-no-records'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
<BoxContainer Orientation="Vertical"
Margin="10 10"
MinWidth="250"
MaxWidth="250">
<Label Name="RecordListingTitle"
Text="{Loc 'criminal-records-console-records-list-title'}"
HorizontalExpand="True"
Align="Center" />
<Label Name="NoRecords"
Text="{Loc 'criminal-records-console-no-records'}"
HorizontalExpand="True"
Align="Center"
FontColorOverride="DarkGray" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="RecordListing"/> <!-- Populated when loading state -->
<ItemList Name="RecordListing" />
<!-- Populated when loading state -->
</ScrollContainer>
</BoxContainer>
<Label Name="RecordUnselected" Text="{Loc 'criminal-records-console-select-record-info'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
<Label Name="RecordUnselected"
Text="{Loc 'criminal-records-console-select-record-info'}"
HorizontalExpand="True"
Align="Center"
FontColorOverride="DarkGray" />
<!-- Selected record info -->
<BoxContainer Name="PersonContainer" Orientation="Vertical" Margin="5" Visible="False">
<Label Name="PersonName" StyleClasses="LabelBig"/>
<Label Name="PersonPrints"/>
<Label Name="PersonDna"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
<Label Name="StatusLabel" Text="{Loc 'criminal-records-console-status'}" FontColorOverride="DarkGray"/>
<OptionButton Name="StatusOptionButton"/> <!-- Populated in constructor -->
<BoxContainer Name="PersonContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="5"
Visible="False">
<Label Name="PersonName"
Margin="0 0 0 5"
StyleClasses="LabelBig" />
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'crew-monitoring-user-interface-job'}:"
FontColorOverride="DarkGray" />
<Label Text=":"
FontColorOverride="DarkGray" />
<TextureRect Name="PersonJobIcon"
TextureScale="2 2"
Margin="6 0"
VerticalAlignment="Center" />
<Label Name="PersonJob" />
</BoxContainer>
<RichTextLabel Name="WantedReason" Visible="False"/>
<Button Name="HistoryButton" Text="{Loc 'criminal-records-console-crime-history'}"/>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'general-station-record-prints-filter'}"
FontColorOverride="DarkGray" />
<Label Text=":"
Margin="0 0 6 0"
FontColorOverride="DarkGray" />
<Label Name="PersonPrints" />
</BoxContainer>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'general-station-record-dna-filter'}"
FontColorOverride="DarkGray" />
<Label Text=":"
Margin="0 0 6 0"
FontColorOverride="DarkGray" />
<Label Name="PersonDna" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider"
Margin="0 5 0 5" />
<BoxContainer Orientation="Horizontal"
Margin="0 5 0 5">
<Label Name="StatusLabel"
Text="{Loc 'criminal-records-console-status'}"
FontColorOverride="DarkGray" />
<Label Text=":"
FontColorOverride="DarkGray" />
<Label Name="PersonStatus"
FontColorOverride="DarkGray" />
<AnimatedTextureRect Name="PersonStatusTX"
Margin="8 0" />
<OptionButton Name="StatusOptionButton"
MinWidth="130" />
<!-- Populated in constructor -->
</BoxContainer>
<RichTextLabel Name="WantedReason"
Visible="False"
MaxWidth="425" />
<Button Name="HistoryButton"
Text="{Loc 'criminal-records-console-crime-history'}"
Margin="0 5" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<OptionButton
Name="CrewListFilter"
MinWidth="250"
Margin="10 0 10 0" />
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal"
Margin="10 2 5 0"
VerticalAlignment="Bottom">
<Label Text="{Loc 'criminal-records-console-flavor-left'}"
StyleClasses="WindowFooterText" />
<Label Text="{Loc 'criminal-records-console-flavor-right'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19" />
</BoxContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -13,6 +13,9 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
namespace Content.Client.CriminalRecords;
@@ -24,6 +27,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
private readonly IPrototypeManager _proto;
private readonly IRobustRandom _random;
private readonly AccessReaderSystem _accessReader;
[Dependency] private readonly IEntityManager _entManager = default!;
private readonly SpriteSystem _spriteSystem;
public readonly EntityUid Console;
@@ -33,10 +38,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public Action<uint?>? OnKeySelected;
public Action<StationRecordFilterType, string>? OnFiltersChanged;
public Action<SecurityStatus>? OnStatusSelected;
public Action<uint>? OnCheckStatus;
public Action<CriminalRecord, bool, bool>? OnHistoryUpdated;
public Action? OnHistoryClosed;
public Action<SecurityStatus, string>? OnDialogConfirmed;
public Action<SecurityStatus>? OnStatusFilterPressed;
private uint _maxLength;
private bool _access;
private uint? _selectedKey;
@@ -46,6 +53,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
private StationRecordFilterType _currentFilterType;
private SecurityStatus _currentCrewListFilter;
public CriminalRecordsConsoleWindow(EntityUid console, uint maxLength, IPlayerManager playerManager, IPrototypeManager prototypeManager, IRobustRandom robustRandom, AccessReaderSystem accessReader)
{
RobustXamlLoader.Load(this);
@@ -55,10 +64,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
_proto = prototypeManager;
_random = robustRandom;
_accessReader = accessReader;
IoCManager.InjectDependencies(this);
_spriteSystem = _entManager.System<SpriteSystem>();
_maxLength = maxLength;
_currentFilterType = StationRecordFilterType.Name;
_currentCrewListFilter = SecurityStatus.None;
OpenCentered();
foreach (var item in Enum.GetValues<StationRecordFilterType>())
@@ -71,6 +84,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
AddStatusSelect(status);
}
//Populate status to filter crew list
foreach (var item in Enum.GetValues<SecurityStatus>())
{
CrewListFilter.AddItem(GetCrewListFilterLocals(item), (int)item);
}
OnClose += () => _reasonDialog?.Close();
RecordListing.OnItemSelected += args =>
@@ -97,6 +116,20 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
}
};
//Select Status to filter crew
CrewListFilter.OnItemSelected += eventArgs =>
{
var type = (SecurityStatus)eventArgs.Id;
if (_currentCrewListFilter != type)
{
_currentCrewListFilter = type;
StatusFilterPressed(type);
}
};
FilterText.OnTextEntered += args =>
{
FilterListingOfRecords(args.Text);
@@ -104,16 +137,21 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
StatusOptionButton.OnItemSelected += args =>
{
SetStatus((SecurityStatus) args.Id);
SetStatus((SecurityStatus)args.Id);
};
HistoryButton.OnPressed += _ =>
{
if (_selectedRecord is {} record)
if (_selectedRecord is { } record)
OnHistoryUpdated?.Invoke(record, _access, true);
};
}
public void StatusFilterPressed(SecurityStatus statusSelected)
{
OnStatusFilterPressed?.Invoke(statusSelected);
}
public void UpdateState(CriminalRecordsConsoleState state)
{
if (state.Filter != null)
@@ -129,10 +167,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
}
}
if (state.FilterStatus != _currentCrewListFilter)
{
_currentCrewListFilter = state.FilterStatus;
}
_selectedKey = state.SelectedKey;
FilterType.SelectId((int)_currentFilterType);
CrewListFilter.SelectId((int)_currentCrewListFilter);
NoRecords.Visible = state.RecordListing == null || state.RecordListing.Count == 0;
PopulateRecordListing(state.RecordListing);
@@ -179,7 +221,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
// in parallel to synchronize the items in RecordListing with `entries`.
int i = RecordListing.Count - 1;
int j = entries.Count - 1;
while(i >= 0 && j >= 0)
while (i >= 0 && j >= 0)
{
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
if (strcmp == 0)
@@ -212,23 +254,44 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
while (j >= 0)
{
RecordListing.Insert(0, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
j--;
}
}
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
{
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");
var na = Loc.GetString("generic-not-available-shorthand");
PersonName.Text = stationRecord.Name;
PersonPrints.Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", stationRecord.Fingerprint ?? na));
PersonDna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", stationRecord.DNA ?? na));
PersonJob.Text = stationRecord.JobTitle ?? na;
StatusOptionButton.SelectId((int) criminalRecord.Status);
if (criminalRecord.Reason is {} reason)
// Job icon
if (_proto.TryIndex<JobIconPrototype>(stationRecord.JobIcon, out var proto))
{
PersonJobIcon.Texture = _spriteSystem.Frame0(proto.Icon);
}
PersonPrints.Text = stationRecord.Fingerprint ?? Loc.GetString("generic-not-available-shorthand");
PersonDna.Text = stationRecord.DNA ?? Loc.GetString("generic-not-available-shorthand");
if (criminalRecord.Status != SecurityStatus.None)
{
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/security_icons.rsi"), GetStatusIcon(criminalRecord.Status));
}
PersonStatusTX.SetFromSpriteSpecifier(specifier);
PersonStatusTX.DisplayRect.TextureScale = new Vector2(3f, 3f);
StatusOptionButton.SelectId((int)criminalRecord.Status);
if (criminalRecord.Reason is { } reason)
{
var message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-wanted-reason"));
if (criminalRecord.Status == SecurityStatus.Suspected)
{
message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-suspected-reason"));
}
message.AddText($": {reason}");
WantedReason.SetMessage(message);
WantedReason.Visible = true;
}
@@ -288,9 +351,37 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
_reasonDialog.OnClose += () => { _reasonDialog = null; };
}
private string GetStatusIcon(SecurityStatus status)
{
return status switch
{
SecurityStatus.Paroled => "hud_paroled",
SecurityStatus.Wanted => "hud_wanted",
SecurityStatus.Detained => "hud_incarcerated",
SecurityStatus.Discharged => "hud_discharged",
SecurityStatus.Suspected => "hud_suspected",
_ => "SecurityIconNone"
};
}
private string GetTypeFilterLocals(StationRecordFilterType type)
{
return Loc.GetString($"criminal-records-{type.ToString().ToLower()}-filter");
}
private string GetCrewListFilterLocals(SecurityStatus type)
{
string result;
// If "NONE" override to "show all"
if (type == SecurityStatus.None)
{
result = Loc.GetString("criminal-records-console-show-all");
}
else
{
result = Loc.GetString($"criminal-records-status-{type.ToString().ToLower()}");
}
return result;
}
}

View File

@@ -21,112 +21,131 @@ public sealed class DoorSystem : SharedDoorSystem
protected override void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
{
var comp = ent.Comp;
comp.OpenSpriteStates = new(2);
comp.ClosedSpriteStates = new(2);
comp.OpenSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.ClosedSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.OpenSpriteStates.Add((DoorVisualLayers.Base, comp.OpenSpriteState));
comp.ClosedSpriteStates.Add((DoorVisualLayers.Base, comp.ClosedSpriteState));
comp.OpeningAnimation = new Animation()
comp.OpeningAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.Base,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f),
},
},
},
};
comp.ClosingAnimation = new Animation()
comp.ClosingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.Base,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f),
},
},
},
};
comp.EmaggingAnimation = new Animation ()
comp.EmaggingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f),
},
},
},
};
}
private void OnAppearanceChange(EntityUid uid, DoorComponent comp, ref AppearanceChangeEvent args)
private void OnAppearanceChange(Entity<DoorComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if(!AppearanceSystem.TryGetData<DoorState>(uid, DoorVisuals.State, out var state, args.Component))
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
if (AppearanceSystem.TryGetData<string>(uid, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
UpdateSpriteLayers(args.Sprite, baseRsi);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
UpdateAppearanceForDoorState(entity, args.Sprite, state);
}
private void UpdateAppearanceForDoorState(Entity<DoorComponent> entity, SpriteComponent sprite, DoorState state)
{
sprite.DrawDepth = state is DoorState.Open ? entity.Comp.OpenDrawDepth : entity.Comp.ClosedDrawDepth;
switch (state)
{
case DoorState.Open:
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
{
sprite.LayerSetState(layer, layerState);
}
return;
case DoorState.Closed:
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
{
sprite.LayerSetState(layer, layerState);
}
return;
case DoorState.Opening:
if (entity.Comp.OpeningAnimationTime == 0.0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Closing:
if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Denying:
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Emagging:
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.AnimationKey);
return;
}
}
private void UpdateSpriteLayers(SpriteComponent sprite, string baseRsi)
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
}
foreach (var layer in args.Sprite.AllLayers)
{
layer.Rsi = res?.RSI;
}
return;
}
TryComp<AnimationPlayerComponent>(uid, out var animPlayer);
if (_animationSystem.HasRunningAnimation(uid, animPlayer, DoorComponent.AnimationKey))
_animationSystem.Stop(uid, animPlayer, DoorComponent.AnimationKey); // Halt all running anomations.
args.Sprite.DrawDepth = comp.ClosedDrawDepth;
switch(state)
{
case DoorState.Open:
args.Sprite.DrawDepth = comp.OpenDrawDepth;
foreach(var (layer, layerState) in comp.OpenSpriteStates)
{
args.Sprite.LayerSetState(layer, layerState);
}
break;
case DoorState.Closed:
foreach(var (layer, layerState) in comp.ClosedSpriteStates)
{
args.Sprite.LayerSetState(layer, layerState);
}
break;
case DoorState.Opening:
if (animPlayer != null && comp.OpeningAnimationTime != 0.0)
_animationSystem.Play((uid, animPlayer), (Animation)comp.OpeningAnimation, DoorComponent.AnimationKey);
break;
case DoorState.Closing:
if (animPlayer != null && comp.ClosingAnimationTime != 0.0 && comp.CurrentlyCrushing.Count == 0)
_animationSystem.Play((uid, animPlayer), (Animation)comp.ClosingAnimation, DoorComponent.AnimationKey);
break;
case DoorState.Denying:
if (animPlayer != null)
_animationSystem.Play((uid, animPlayer), (Animation)comp.DenyingAnimation, DoorComponent.AnimationKey);
break;
case DoorState.Welded:
break;
case DoorState.Emagging:
if (animPlayer != null)
_animationSystem.Play((uid, animPlayer), (Animation)comp.EmaggingAnimation, DoorComponent.AnimationKey);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid door visual state {state}");
}
sprite.BaseRSI = res.RSI;
}
}

View File

@@ -1,5 +1,8 @@
using Content.Shared.Magic;
using Content.Shared.Magic.Events;
namespace Content.Client.Magic;
public sealed class MagicSystem : SharedMagicSystem;
public sealed class MagicSystem : SharedMagicSystem
{
}

View File

@@ -4,6 +4,7 @@ using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Client.Orbit;
@@ -11,8 +12,8 @@ public sealed class OrbitVisualsSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AnimationPlayerSystem _animations = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly string _orbitAnimationKey = "orbiting";
private readonly string _orbitStopKey = "orbiting_stop";
public override void Initialize()
@@ -21,11 +22,11 @@ public sealed class OrbitVisualsSystem : EntitySystem
SubscribeLocalEvent<OrbitVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<OrbitVisualsComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<OrbitVisualsComponent, AnimationCompletedEvent>(OnAnimationCompleted);
}
private void OnComponentInit(EntityUid uid, OrbitVisualsComponent component, ComponentInit args)
{
_robustRandom.SetSeed((int)_timing.CurTime.TotalMilliseconds);
component.OrbitDistance =
_robustRandom.NextFloat(0.75f * component.OrbitDistance, 1.25f * component.OrbitDistance);
@@ -38,15 +39,10 @@ public sealed class OrbitVisualsSystem : EntitySystem
}
var animationPlayer = EnsureComp<AnimationPlayerComponent>(uid);
if (_animations.HasRunningAnimation(uid, animationPlayer, _orbitAnimationKey))
return;
if (_animations.HasRunningAnimation(uid, animationPlayer, _orbitStopKey))
{
_animations.Stop(uid, animationPlayer, _orbitStopKey);
_animations.Stop((uid, animationPlayer), _orbitStopKey);
}
_animations.Play(uid, animationPlayer, GetOrbitAnimation(component), _orbitAnimationKey);
}
private void OnComponentRemove(EntityUid uid, OrbitVisualsComponent component, ComponentRemove args)
@@ -57,14 +53,9 @@ public sealed class OrbitVisualsSystem : EntitySystem
sprite.EnableDirectionOverride = false;
var animationPlayer = EnsureComp<AnimationPlayerComponent>(uid);
if (_animations.HasRunningAnimation(uid, animationPlayer, _orbitAnimationKey))
{
_animations.Stop(uid, animationPlayer, _orbitAnimationKey);
}
if (!_animations.HasRunningAnimation(uid, animationPlayer, _orbitStopKey))
{
_animations.Play(uid, animationPlayer, GetStopAnimation(component, sprite), _orbitStopKey);
_animations.Play((uid, animationPlayer), GetStopAnimation(component, sprite), _orbitStopKey);
}
}
@@ -74,7 +65,8 @@ public sealed class OrbitVisualsSystem : EntitySystem
foreach (var (orbit, sprite) in EntityManager.EntityQuery<OrbitVisualsComponent, SpriteComponent>())
{
var angle = new Angle(Math.PI * 2 * orbit.Orbit);
var progress = (float)(_timing.CurTime.TotalSeconds / orbit.OrbitLength) % 1;
var angle = new Angle(Math.PI * 2 * progress);
var vec = angle.RotateVec(new Vector2(orbit.OrbitDistance, 0));
sprite.Rotation = angle;
@@ -82,38 +74,6 @@ public sealed class OrbitVisualsSystem : EntitySystem
}
}
private void OnAnimationCompleted(EntityUid uid, OrbitVisualsComponent component, AnimationCompletedEvent args)
{
if (args.Key == _orbitAnimationKey && TryComp(uid, out AnimationPlayerComponent? animationPlayer))
{
_animations.Play(uid, animationPlayer, GetOrbitAnimation(component), _orbitAnimationKey);
}
}
private Animation GetOrbitAnimation(OrbitVisualsComponent component)
{
var length = component.OrbitLength;
return new Animation()
{
Length = TimeSpan.FromSeconds(length),
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(OrbitVisualsComponent),
Property = nameof(OrbitVisualsComponent.Orbit),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(0.0f, 0f),
new AnimationTrackProperty.KeyFrame(1.0f, length),
},
InterpolationMode = AnimationInterpolationMode.Linear
}
}
};
}
private Animation GetStopAnimation(OrbitVisualsComponent component, SpriteComponent sprite)
{
var length = component.OrbitStopLength;

View File

@@ -56,35 +56,35 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
protected virtual void DeactivateInternal() { }
private void OnStartup(EntityUid uid, T component, ComponentStartup args)
private void OnStartup(Entity<T> ent, ref ComponentStartup args)
{
RefreshOverlay(uid);
RefreshOverlay();
}
private void OnRemove(EntityUid uid, T component, ComponentRemove args)
private void OnRemove(Entity<T> ent, ref ComponentRemove args)
{
RefreshOverlay(uid);
RefreshOverlay();
}
private void OnPlayerAttached(LocalPlayerAttachedEvent args)
{
RefreshOverlay(args.Entity);
RefreshOverlay();
}
private void OnPlayerDetached(LocalPlayerDetachedEvent args)
{
if (_player.LocalSession?.AttachedEntity == null)
if (_player.LocalSession?.AttachedEntity is null)
Deactivate();
}
private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args)
private void OnCompEquip(Entity<T> ent, ref GotEquippedEvent args)
{
RefreshOverlay(args.Equipee);
RefreshOverlay();
}
private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args)
private void OnCompUnequip(Entity<T> ent, ref GotUnequippedEvent args)
{
RefreshOverlay(args.Equipee);
RefreshOverlay();
}
private void OnRoundRestart(RoundRestartCleanupEvent args)
@@ -92,24 +92,24 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
Deactivate();
}
protected virtual void OnRefreshEquipmentHud(EntityUid uid, T component, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
protected virtual void OnRefreshEquipmentHud(Entity<T> ent, ref InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
{
OnRefreshComponentHud(uid, component, args.Args);
OnRefreshComponentHud(ent, ref args.Args);
}
protected virtual void OnRefreshComponentHud(EntityUid uid, T component, RefreshEquipmentHudEvent<T> args)
protected virtual void OnRefreshComponentHud(Entity<T> ent, ref RefreshEquipmentHudEvent<T> args)
{
args.Active = true;
args.Components.Add(component);
args.Components.Add(ent.Comp);
}
protected void RefreshOverlay(EntityUid uid)
protected void RefreshOverlay()
{
if (uid != _player.LocalSession?.AttachedEntity)
if (_player.LocalSession?.AttachedEntity is not { } entity)
return;
var ev = new RefreshEquipmentHudEvent<T>(TargetSlots);
RaiseLocalEvent(uid, ev);
RaiseLocalEvent(entity, ref ev);
if (ev.Active)
Update(ev);

View File

@@ -28,7 +28,7 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
private void OnHandleState(Entity<ShowHealthBarsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component)

View File

@@ -47,7 +47,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
private void OnHandleState(Entity<ShowHealthIconsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
RefreshOverlay();
}
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)

View File

@@ -2,49 +2,128 @@
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.ParticleAccelerator.UI"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'particle-accelerator-control-menu-device-version-label'}"
MinSize="420 320"
SetSize="420 320">
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 5">
MinSize="320 120">
<!-- Main Container -->
<BoxContainer Orientation="Vertical"
VerticalExpand="True">
<!-- Sub-Main container -->
<BoxContainer Orientation="Horizontal"
VerticalExpand="True"
HorizontalExpand="True">
<!-- Info part -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
Margin="8">
<!-- Info -->
<BoxContainer Orientation="Vertical"
SeparationOverride="4">
<!-- Status -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StatusLabel" HorizontalExpand="True"/>
<Control MinWidth="8"/>
<RichTextLabel Name="StatusStateLabel"/>
</BoxContainer>
<Control MinHeight="5"/>
<!-- Power -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="PowerLabel" Margin="0 0 20 0" HorizontalExpand="True" VerticalAlignment="Center"/>
<Button Name="OffButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-off-button'}" StyleClasses="OpenRight"/>
<Button Name="OnButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-on-button'}" StyleClasses="OpenLeft"/>
<RichTextLabel Name="PowerLabel"
HorizontalExpand="True"
VerticalAlignment="Center"/>
<Control MinWidth="8"/>
<Button Name="OffButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-off-button'}"
StyleClasses="OpenRight"/>
<Button Name="OnButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-on-button'}"
StyleClasses="OpenLeft"/>
</BoxContainer>
<Control MinHeight="5"/>
<!-- Strenght -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StrengthLabel" Margin="0 0 20 0" HorizontalExpand="True" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<RichTextLabel Name="StrengthLabel"
HorizontalExpand="True"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
<Control MinWidth="8"/>
<SpinBox Name="StateSpinBox" Value="0"/>
</BoxContainer>
<Control MinHeight="5"/>
<!-- Power -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="DrawLabel" HorizontalExpand="True"/>
<Control MinWidth="8"/>
<RichTextLabel Name="DrawValueLabel"/>
</BoxContainer>
<Control MinHeight="10" VerticalExpand="True"/>
<BoxContainer Name="AlarmControl" Orientation="Vertical" VerticalAlignment="Center" Visible="False">
<RichTextLabel Name="BigAlarmLabel" HorizontalAlignment="Center"/>
<RichTextLabel Name="BigAlarmLabelTwo" HorizontalAlignment="Center"/>
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
</BoxContainer>
<Control MinHeight="10" VerticalExpand="True"/>
<Control MinHeight="8" VerticalExpand="True"/> <!-- Filler -->
<!-- Alarm -->
<BoxContainer Name="AlarmControl"
Orientation="Vertical"
VerticalAlignment="Center"
Visible="False">
<controls:StripeBack Margin="-8 0">
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="BigAlarmLabel"
HorizontalAlignment="Center"/>
<RichTextLabel Name="BigAlarmLabelTwo"
HorizontalAlignment="Center"/>
</BoxContainer>
<customControls:VSeparator Margin="0 0 0 10"/>
<BoxContainer Orientation="Vertical" Margin="20 0 20 0" VerticalAlignment="Center">
<PanelContainer Name="BackPanel" HorizontalAlignment="Center">
</controls:StripeBack>
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}"
HorizontalAlignment="Center"
StyleClasses="LabelSubText"/>
</BoxContainer>
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 -8" HorizontalAlignment="Right"/>
<!-- PA Visual part -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Center"
Margin="8">
<PanelContainer Name="BackPanel"
HorizontalAlignment="Center">
<PanelContainer.PanelOverride>
<gfx:StyleBoxTexture Modulate="#202023" PatchMarginBottom="10" PatchMarginLeft="10" PatchMarginRight="10" PatchMarginTop="10"/>
<gfx:StyleBoxTexture Modulate="#202023"
PatchMarginBottom="8"
PatchMarginLeft="8"
PatchMarginRight="8"
PatchMarginTop="8"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" HorizontalAlignment="Center" VerticalExpand="True">
<GridContainer Columns="3" VSeparationOverride="0" HSeparationOverride="0" HorizontalAlignment="Center">
<BoxContainer Orientation="Vertical"
SeparationOverride="6"
VerticalExpand="True"
VerticalAlignment="Stretch"
HorizontalExpand="True"
HorizontalAlignment="Center">
<!-- PA Visualisation -->
<GridContainer Columns="3"
VSeparationOverride="0"
HSeparationOverride="0"
HorizontalAlignment="Center">
<Control/>
<ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/>
<Control/>
@@ -58,17 +137,47 @@
<ui:PASegmentControl Name="EmitterForeTexture" BaseState="emitter_fore"/>
<ui:PASegmentControl Name="EmitterPortTexture" BaseState="emitter_port"/>
</GridContainer>
<Control MinHeight="5"/>
<Button Name="ScanButton" Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}" HorizontalAlignment="Center"/>
<Button Name="ScanButton"
Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}"
HorizontalAlignment="Center"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Bottom">
<controls:StripeBack>
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 4 0 4"/>
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}"
HorizontalAlignment="Center"
StyleClasses="LabelSubText"
Margin="0 4"/>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal" Margin="12 0 0 0">
<Label Text="{Loc 'particle-accelerator-control-menu-foo-bar-baz'}" StyleClasses="LabelSubText"/>
<BoxContainer Orientation="Horizontal"
Margin="12 0 6 2"
VerticalAlignment="Bottom">
<!-- Footer title -->
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-left'}"
StyleClasses="WindowFooterText" />
<!-- Version -->
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-right'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 4 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -81,13 +81,19 @@ public sealed partial class NavScreen : BoxContainer
// Get the positive reduced angle.
var displayRot = -worldRot.Reduced();
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
GridPosition.Text = Loc.GetString("shuttle-console-position-value",
("X", $"{worldPos.X:0.0}"),
("Y", $"{worldPos.Y:0.0}"));
GridOrientation.Text = Loc.GetString("shuttle-console-orientation-value",
("angle", $"{displayRot.Degrees:0.0}"));
var gridVelocity = gridBody.LinearVelocity;
gridVelocity = displayRot.RotateVec(gridVelocity);
// Get linear velocity relative to the console entity
GridLinearVelocity.Text = $"{gridVelocity.X + 10f * float.Epsilon:0.0}, {gridVelocity.Y + 10f * float.Epsilon:0.0}";
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity + 10f * float.Epsilon:0.0}";
GridLinearVelocity.Text = Loc.GetString("shuttle-console-linear-velocity-value",
("X", $"{gridVelocity.X + 10f * float.Epsilon:0.0}"),
("Y", $"{gridVelocity.Y + 10f * float.Epsilon:0.0}"));
GridAngularVelocity.Text = Loc.GetString("shuttle-console-angular-velocity-value",
("angularVelocity", $"{-MathHelper.RadiansToDegrees(gridBody.AngularVelocity) + 10f * float.Epsilon:0.0}"));
}
}

View File

@@ -144,7 +144,7 @@ namespace Content.Client.Viewport
_inputManager.ViewportKeyEvent(this, args);
}
protected override void Draw(DrawingHandleScreen handle)
protected override void Draw(IRenderHandle handle)
{
EnsureViewportCreated();
@@ -170,7 +170,7 @@ namespace Content.Client.Viewport
var drawBox = GetDrawBox();
var drawBoxGlobal = drawBox.Translated(GlobalPixelPosition);
_viewport.RenderScreenOverlaysBelow(handle, this, drawBoxGlobal);
handle.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox);
handle.DrawingHandleScreen.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox);
_viewport.RenderScreenOverlaysAbove(handle, this, drawBoxGlobal);
}

View File

@@ -138,6 +138,7 @@ public sealed partial class MeleeWeaponSystem
const float length = 0.15f;
var startOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
var endOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance));
sprite.Rotation += spriteRotation;
return new Animation()
{

View File

@@ -70,7 +70,9 @@ namespace Content.IntegrationTests.Tests
"Amber",
"Loop",
"Plasma",
"Elkridge"
"Elkridge",
"Convex"
};
/// <summary>

View File

@@ -5,6 +5,7 @@ using Content.Server.Store.Systems;
using Content.Server.Traitor.Uplink;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Content.Shared.StoreDiscount.Components;
@@ -64,6 +65,7 @@ public sealed class StoreTests
await server.WaitAssertion(() =>
{
var invSystem = entManager.System<InventorySystem>();
var mindSystem = entManager.System<SharedMindSystem>();
human = entManager.SpawnEntity("HumanUniformDummy", coordinates);
uniform = entManager.SpawnEntity("UniformDummy", coordinates);
@@ -72,6 +74,9 @@ public sealed class StoreTests
Assert.That(invSystem.TryEquip(human, uniform, "jumpsuit"));
Assert.That(invSystem.TryEquip(human, pda, "id"));
var mind = mindSystem.CreateMind(null);
mindSystem.TransferTo(mind, human, mind: mind);
FixedPoint2 originalBalance = 20;
uplinkSystem.AddUplink(human, originalBalance, null, true);

View File

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

View File

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

View File

@@ -165,6 +165,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
null);
await _db.AddServerBanAsync(banDef);
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher!
var adminName = banningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");

View File

@@ -892,5 +892,36 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
};
args.Verbs.Add(superslip);
var omniaccentName = Loc.GetString("admin-smite-omni-accent-name").ToLowerInvariant();
Verb omniaccent = new()
{
Text = omniaccentName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Interface/Actions/voice-mask.rsi"), "icon"),
Act = () =>
{
EnsureComp<BarkAccentComponent>(args.Target);
EnsureComp<BleatingAccentComponent>(args.Target);
EnsureComp<FrenchAccentComponent>(args.Target);
EnsureComp<GermanAccentComponent>(args.Target);
EnsureComp<LizardAccentComponent>(args.Target);
EnsureComp<MobsterAccentComponent>(args.Target);
EnsureComp<MothAccentComponent>(args.Target);
EnsureComp<OwOAccentComponent>(args.Target);
EnsureComp<SkeletonAccentComponent>(args.Target);
EnsureComp<SouthernAccentComponent>(args.Target);
EnsureComp<SpanishAccentComponent>(args.Target);
EnsureComp<StutteringAccentComponent>(args.Target);
if (_random.Next(0, 8) == 0)
{
EnsureComp<BackwardsAccentComponent>(args.Target); // was asked to make this at a low chance idk
}
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
};
args.Verbs.Add(omniaccent);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1112,7 +1112,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.SingleOrDefaultAsync());
}
public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
{
await using var db = await GetDb();
@@ -1122,7 +1122,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return;
}
dbPlayer.LastReadRules = date.UtcDateTime;
dbPlayer.LastReadRules = date?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}

View File

@@ -282,7 +282,7 @@ namespace Content.Server.Database
#region Rules
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
Task SetLastReadRules(NetUserId player, DateTimeOffset? time);
#endregion
@@ -830,7 +830,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetLastReadRules(player));
}
public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
public Task SetLastReadRules(NetUserId player, DateTimeOffset? time)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetLastReadRules(player, time));

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.UserInterface;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.Lathe;
using Content.Shared.Materials;
@@ -42,6 +43,7 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly PopupSystem _popup = default!;
@@ -292,7 +294,7 @@ namespace Content.Server.Lathe
{
if (uid != args.Lathe || !TryComp<TechnologyDatabaseComponent>(uid, out var technologyDatabase))
return;
if (!args.getUnavailable && !HasComp<EmaggedComponent>(uid))
if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
return;
foreach (var recipe in component.EmagDynamicRecipes)
{

View File

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

View File

@@ -72,7 +72,10 @@ public sealed class HealingSystem : EntitySystem
_bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
if (isBleeding != bloodstream.BleedAmount > 0)
{
_popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User);
var popup = (args.User == entity.Owner)
? Loc.GetString("medical-item-stop-bleeding-self")
: Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
_popupSystem.PopupEntity(popup, entity, args.User);
}
}

View File

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

View File

@@ -1,5 +1,5 @@
using Content.Server.Chat.Systems;
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Emag.Components;
@@ -55,34 +55,11 @@ public sealed partial class MedibotInjectOperator : HTNOperator
if (!_entMan.TryGetComponent<MedibotComponent>(owner, out var botComp))
return HTNOperatorStatus.Failed;
if (!_entMan.TryGetComponent<DamageableComponent>(target, out var damage))
if (!_medibot.CheckInjectable((owner, botComp), target) || !_medibot.TryInject((owner, botComp), target))
return HTNOperatorStatus.Failed;
if (!_solutionContainer.TryGetInjectableSolution(target, out var injectable, out _))
return HTNOperatorStatus.Failed;
if (!_interaction.InRangeUnobstructed(owner, target))
return HTNOperatorStatus.Failed;
var total = damage.TotalDamage;
// always inject healthy patients when emagged
if (total == 0 && !_entMan.HasComponent<EmaggedComponent>(owner))
return HTNOperatorStatus.Failed;
if (!_entMan.TryGetComponent<MobStateComponent>(target, out var mobState))
return HTNOperatorStatus.Failed;
var state = mobState.CurrentState;
if (!_medibot.TryGetTreatment(botComp, mobState.CurrentState, out var treatment) || !treatment.IsValid(total))
return HTNOperatorStatus.Failed;
_entMan.EnsureComponent<NPCRecentlyInjectedComponent>(target);
_solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _);
_popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target);
_audio.PlayPvs(botComp.InjectSound, target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: true, hideLog: true);
return HTNOperatorStatus.Finished;
}
}

View File

@@ -1,6 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Damage;

View File

@@ -1,4 +1,4 @@
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
namespace Content.Server.NPC.Systems;

View File

@@ -25,6 +25,13 @@ namespace Content.Server.Nuke
[ViewVariables(VVAccess.ReadWrite)]
public int Timer = 300;
/// <summary>
/// If the nuke is disarmed, this sets the minimum amount of time the timer can have.
/// The remaining time will reset to this value if it is below it.
/// </summary>
[DataField]
public int MinimumTime = 180;
/// <summary>
/// How long until the bomb can arm again after deactivation.
/// Used to prevent announcements spam.

View File

@@ -522,6 +522,9 @@ public sealed class NukeSystem : EntitySystem
_sound.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
// reset nuke remaining time to either itself or the minimum time, whichever is higher
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
// disable sound and reset it
component.PlayedAlertSound = false;
component.AlertAudioStream = _audio.Stop(component.AlertAudioStream);

View File

@@ -21,6 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems;
public sealed class FatExtractorSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -36,8 +37,13 @@ public sealed class FatExtractorSystem : EntitySystem
private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
args.Repeatable = false;
}
private void OnClosed(EntityUid uid, FatExtractorComponent component, ref StorageAfterCloseEvent args)
@@ -103,7 +109,7 @@ public sealed class FatExtractorSystem : EntitySystem
if (_hunger.GetHunger(hunger) < component.NutritionPerSecond)
return false;
if (hunger.CurrentThreshold < component.MinHungerThreshold && !HasComp<EmaggedComponent>(uid))
if (hunger.CurrentThreshold < component.MinHungerThreshold && !_emag.CheckFlag(uid, EmagType.Interaction))
return false;
return true;

View File

@@ -24,6 +24,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -63,7 +64,7 @@ namespace Content.Server.Nutrition.EntitySystems
forced = false;
}
if (entity.Comp.ExplodeOnUse || HasComp<EmaggedComponent>(entity.Owner))
if (entity.Comp.ExplodeOnUse || _emag.CheckFlag(entity, EmagType.Interaction))
{
_explosionSystem.QueueExplosion(entity.Owner, "Default", entity.Comp.ExplosionIntensity, 0.5f, 3, canCreateVacuum: false);
EntityManager.DeleteEntity(entity);
@@ -161,8 +162,15 @@ namespace Content.Server.Nutrition.EntitySystems
args.Args.Target.Value);
}
}
private void OnEmagged(Entity<VapeComponent> entity, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(entity, EmagType.Interaction))
return;
args.Handled = true;
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Systems;
using Content.Shared.APC;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Popups;
using Content.Shared.Rounding;
@@ -19,6 +18,7 @@ public sealed class ApcSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -111,7 +111,12 @@ public sealed class ApcSystem : EntitySystem
private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args)
{
// no fancy conditions
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}
@@ -170,7 +175,7 @@ public sealed class ApcSystem : EntitySystem
private ApcChargeState CalcChargeState(EntityUid uid, PowerState.Battery battery)
{
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
return ApcChargeState.Emag;
if (battery.CurrentStorage / battery.Capacity > ApcComponent.HighPowerThreshold)

View File

@@ -3,6 +3,7 @@ using Content.Server.Research.Components;
using Content.Shared.UserInterface;
using Content.Shared.Access.Components;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
@@ -11,6 +12,8 @@ namespace Content.Server.Research.Systems;
public sealed partial class ResearchSystem
{
[Dependency] private readonly EmagSystem _emag = default!;
private void InitializeConsole()
{
SubscribeLocalEvent<ResearchConsoleComponent, ConsoleUnlockTechnologyMessage>(OnConsoleUnlock);
@@ -18,6 +21,7 @@ public sealed partial class ResearchSystem
SubscribeLocalEvent<ResearchConsoleComponent, ResearchServerPointsChangedEvent>(OnPointsChanged);
SubscribeLocalEvent<ResearchConsoleComponent, ResearchRegistrationChangedEvent>(OnConsoleRegistrationChanged);
SubscribeLocalEvent<ResearchConsoleComponent, TechnologyDatabaseModifiedEvent>(OnConsoleDatabaseModified);
SubscribeLocalEvent<ResearchConsoleComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnConsoleUnlock(EntityUid uid, ResearchConsoleComponent component, ConsoleUnlockTechnologyMessage args)
@@ -39,7 +43,7 @@ public sealed partial class ResearchSystem
if (!UnlockTechnology(uid, args.Id, act))
return;
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
var getIdentityEvent = new TryGetIdentityShortInfoEvent(uid, act);
RaiseLocalEvent(getIdentityEvent);
@@ -100,4 +104,15 @@ public sealed partial class ResearchSystem
UpdateConsoleInterface(uid, component);
}
private void OnEmagged(Entity<ResearchConsoleComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent, EmagType.Interaction))
return;
args.Handled = true;
}
}

View File

@@ -36,7 +36,6 @@ public sealed partial class RevenantSystem
{
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
@@ -343,7 +342,8 @@ public sealed partial class RevenantSystem
_whitelistSystem.IsBlacklistPass(component.MalfunctionBlacklist, ent))
continue;
_emag.DoEmagEffect(uid, ent); //it does not emag itself. adorable.
var ev = new GotEmaggedEvent(uid, EmagType.Interaction | EmagType.Access);
RaiseLocalEvent(ent, ref ev);
}
}
}

View File

@@ -8,6 +8,7 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.Components;
using Content.Shared.Emag.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Silicons.Borgs;
@@ -15,6 +16,8 @@ namespace Content.Server.Silicons.Borgs;
/// <inheritdoc/>
public sealed partial class BorgSystem
{
[Dependency] private readonly EmagSystem _emag = default!;
private void InitializeTransponder()
{
SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
@@ -127,7 +130,7 @@ public sealed partial class BorgSystem
private bool CheckEmagged(EntityUid uid, string name)
{
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
Popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution);
return true;

View File

@@ -6,7 +6,6 @@ using Content.Server.Roles;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
@@ -14,7 +13,6 @@ using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -22,7 +20,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Audio;
namespace Content.Server.Silicons.Laws;
@@ -34,8 +31,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly EmagSystem _emag = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -52,7 +49,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
SubscribeLocalEvent<SiliconLawProviderComponent, IonStormLawsEvent>(OnIonStormLaws);
SubscribeLocalEvent<SiliconLawProviderComponent, MindAddedMessage>(OnLawProviderMindAdded);
SubscribeLocalEvent<SiliconLawProviderComponent, MindRemovedMessage>(OnLawProviderMindRemoved);
SubscribeLocalEvent<SiliconLawProviderComponent, GotEmaggedEvent>(OnEmagLawsAdded);
SubscribeLocalEvent<SiliconLawProviderComponent, SiliconEmaggedEvent>(OnEmagLawsAdded);
}
private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
@@ -135,7 +132,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
private void OnIonStormLaws(EntityUid uid, SiliconLawProviderComponent component, ref IonStormLawsEvent args)
{
// Emagged borgs are immune to ion storm
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
component.Lawset = args.Lawset;
@@ -152,9 +149,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
}
}
private void OnEmagLawsAdded(EntityUid uid, SiliconLawProviderComponent component, ref GotEmaggedEvent args)
private void OnEmagLawsAdded(EntityUid uid, SiliconLawProviderComponent component, ref SiliconEmaggedEvent args)
{
if (component.Lawset == null)
component.Lawset = GetLawset(component.Laws);
@@ -164,7 +160,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
// Add the first emag law before the others
component.Lawset?.Laws.Insert(0, new SiliconLaw
{
LawString = Loc.GetString("law-emag-custom", ("name", Name(args.UserUid)), ("title", Loc.GetString(component.Lawset.ObeysTo))),
LawString = Loc.GetString("law-emag-custom", ("name", Name(args.user)), ("title", Loc.GetString(component.Lawset.ObeysTo))),
Order = 0
});
@@ -176,20 +172,6 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
});
}
protected override void OnGotEmagged(EntityUid uid, EmagSiliconLawComponent component, ref GotEmaggedEvent args)
{
if (component.RequireOpenPanel && TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
return;
base.OnGotEmagged(uid, component, ref args);
NotifyLawsChanged(uid, component.EmaggedSound);
if(_mind.TryGetMind(uid, out var mindId, out _))
EnsureSubvertedSiliconRole(mindId);
_stunSystem.TryParalyze(uid, component.StunTime, true);
}
private void EnsureSubvertedSiliconRole(EntityUid mindId)
{
if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))

View File

@@ -1,7 +1,6 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Events;
using Content.Server.Stunnable.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Events;
using Content.Shared.Examine;

View File

@@ -39,6 +39,7 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private const float WallVendEjectDistanceFromWall = 1f;
@@ -48,7 +49,6 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<VendingMachineComponent, PriceCalculationEvent>(OnVendingPrice);
SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
@@ -123,12 +123,6 @@ namespace Content.Server.VendingMachines
TryUpdateVisualState(uid, vendComponent);
}
private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args)
{
// only emag if there are emag-only items
args.Handled = component.EmaggedInventory.Count > 0;
}
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
{
if (!args.DamageIncreased && component.Broken)
@@ -232,7 +226,7 @@ namespace Content.Server.VendingMachines
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return true;
if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp<EmaggedComponent>(uid))
if (_accessReader.IsAllowed(sender, uid, accessReader))
return true;
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
@@ -422,7 +416,7 @@ namespace Content.Server.VendingMachines
if (!Resolve(uid, ref component))
return null;
if (type == InventoryType.Emagged && HasComp<EmaggedComponent>(uid))
if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
return component.EmaggedInventory.GetValueOrDefault(entryId);
if (type == InventoryType.Contraband && component.Contraband)

View File

@@ -76,7 +76,7 @@ public sealed partial class AccessReaderComponent : Component
/// Whether or not emag interactions have an effect on this.
/// </summary>
[DataField]
public bool BreakOnEmag = true;
public bool BreakOnAccessBreaker = true;
}
[DataDefinition, Serializable, NetSerializable]

View File

@@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
@@ -24,6 +23,7 @@ public sealed class AccessReaderSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedGameTicker _gameTicker = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -71,17 +71,28 @@ public sealed class AccessReaderSystem : EntitySystem
{
if (args.User == null) // AutoLink (and presumably future external linkers) have no user.
return;
if (!HasComp<EmaggedComponent>(uid) && !IsAllowed(args.User.Value, uid, component))
if (!IsAllowed(args.User.Value, uid, component))
args.Cancel();
}
private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
{
if (!reader.BreakOnEmag)
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!reader.BreakOnAccessBreaker)
return;
if (!GetMainAccessReader(uid, out var accessReader))
return;
if (accessReader.Value.Comp.AccessLists.Count < 1)
return;
args.Repeatable = true;
args.Handled = true;
reader.Enabled = false;
reader.AccessLog.Clear();
accessReader.Value.Comp.AccessLists.Clear();
accessReader.Value.Comp.AccessLog.Clear();
Dirty(uid, reader);
}
@@ -135,6 +146,7 @@ public sealed class AccessReaderSystem : EntitySystem
return true;
}
}
return true;
}

View File

@@ -45,3 +45,6 @@ namespace Content.Shared.Access.Systems
}
}
}
[ByRefEvent]
public record struct OnAccessOverriderAccessUpdatedEvent(EntityUid UserUid, bool Handled = false);

View File

@@ -89,6 +89,12 @@ public sealed partial class CCVars
public static readonly CVarDef<bool> ServerBanErasePlayer =
CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// If true, will reset the last time the player has read the rules. This will mean on their next login they will be shown the rules again.
/// </summary>
public static readonly CVarDef<bool> ServerBanResetLastReadRules =
CVarDef.Create("admin.server_ban_reset_last_read_rules", true, CVar.ARCHIVE | CVar.SERVER);
/// <summary>
/// Minimum players sharing a connection required to create an alert. -1 to disable the alert.
/// </summary>

View File

@@ -0,0 +1,67 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo;
/// <summary>
/// A data structure for storing historical information about bounties.
/// </summary>
[DataDefinition, NetSerializable, Serializable]
public readonly partial record struct CargoBountyHistoryData
{
/// <summary>
/// A unique id used to identify the bounty
/// </summary>
[DataField]
public string Id { get; init; } = string.Empty;
/// <summary>
/// Whether this bounty was completed or skipped.
/// </summary>
[DataField]
public BountyResult Result { get; init; } = BountyResult.Completed;
/// <summary>
/// Optional name of the actor that completed/skipped the bounty.
/// </summary>
[DataField]
public string? ActorName { get; init; } = default;
/// <summary>
/// Time when this bounty was completed or skipped
/// </summary>
[DataField]
public TimeSpan Timestamp { get; init; } = TimeSpan.MinValue;
/// <summary>
/// The prototype containing information about the bounty.
/// </summary>
[DataField(required: true)]
public ProtoId<CargoBountyPrototype> Bounty { get; init; } = string.Empty;
public CargoBountyHistoryData(CargoBountyData bounty, BountyResult result, TimeSpan timestamp, string? actorName)
{
Bounty = bounty.Bounty;
Result = result;
Id = bounty.Id;
ActorName = actorName;
Timestamp = timestamp;
}
/// <summary>
/// Covers how a bounty was actually finished.
/// </summary>
public enum BountyResult
{
/// <summary>
/// Bounty was actually fulfilled and the goods sold
/// </summary>
Completed = 0,
/// <summary>
/// Bounty was explicitly skipped by some actor
/// </summary>
Skipped = 1,
}
}

View File

@@ -50,11 +50,13 @@ public sealed partial class CargoBountyConsoleComponent : Component
public sealed class CargoBountyConsoleState : BoundUserInterfaceState
{
public List<CargoBountyData> Bounties;
public List<CargoBountyHistoryData> History;
public TimeSpan UntilNextSkip;
public CargoBountyConsoleState(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
public CargoBountyConsoleState(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
{
Bounties = bounties;
History = history;
UntilNextSkip = untilNextSkip;
}
}

View File

@@ -26,7 +26,8 @@ public sealed class SolutionSpikerSystem : EntitySystem
private void OnInteractUsing(Entity<RefillableSolutionComponent> entity, ref InteractUsingEvent args)
{
TrySpike(args.Used, args.Target, args.User, entity.Comp);
if (TrySpike(args.Used, args.Target, args.User, entity.Comp))
args.Handled = true;
}
/// <summary>
@@ -36,7 +37,7 @@ public sealed class SolutionSpikerSystem : EntitySystem
/// <param name="source">Source of the solution.</param>
/// <param name="target">Target to spike with the solution from source.</param>
/// <param name="user">User spiking the target solution.</param>
private void TrySpike(EntityUid source, EntityUid target, EntityUid user, RefillableSolutionComponent? spikableTarget = null,
private bool TrySpike(EntityUid source, EntityUid target, EntityUid user, RefillableSolutionComponent? spikableTarget = null,
SolutionSpikerComponent? spikableSource = null,
SolutionContainerManagerComponent? managerSource = null,
SolutionContainerManagerComponent? managerTarget = null)
@@ -46,21 +47,23 @@ public sealed class SolutionSpikerSystem : EntitySystem
|| !_solution.TryGetRefillableSolution((target, spikableTarget, managerTarget), out var targetSoln, out var targetSolution)
|| !_solution.TryGetSolution((source, managerSource), spikableSource.SourceSolution, out _, out var sourceSolution))
{
return;
return false;
}
if (targetSolution.Volume == 0 && !spikableSource.IgnoreEmpty)
{
_popup.PopupClient(Loc.GetString(spikableSource.PopupEmpty, ("spiked-entity", target), ("spike-entity", source)), user, user);
return;
return false;
}
if (!_solution.ForceAddSolution(targetSoln.Value, sourceSolution))
return;
return false;
_popup.PopupClient(Loc.GetString(spikableSource.Popup, ("spiked-entity", target), ("spike-entity", source)), user, user);
sourceSolution.RemoveAllSolution();
if (spikableSource.Delete)
QueueDel(source);
return true;
}
}

View File

@@ -46,15 +46,6 @@ public sealed partial class CloningPodComponent : Component
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
public EntProtoId MobSpawnId = "MobAbomination";
/// <summary>
/// Emag sound effects.
/// </summary>
[DataField("sparkSound")]
public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks")
{
Params = AudioParams.Default.WithVolume(8),
};
// TODO: Remove this from here when cloning and/or zombies are refactored
[DataField("screamSound")]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")

View File

@@ -1,7 +1,10 @@
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.CriminalRecords.Components;
using Content.Shared.CriminalRecords;
using Content.Shared.Radio;
using Content.Shared.StationRecords;
using Robust.Shared.Prototypes;
using Content.Shared.Security;
namespace Content.Shared.CriminalRecords.Components;
@@ -31,6 +34,12 @@ public sealed partial class CriminalRecordsConsoleComponent : Component
[DataField]
public StationRecordsFilter? Filter;
/// <summary>
/// Current seleced security status for the filter by criminal status dropdown.
/// </summary>
[DataField]
public SecurityStatus FilterStatus;
/// <summary>
/// Channel to send messages to when someone's status gets changed.
/// </summary>

View File

@@ -35,9 +35,9 @@ public sealed class CriminalRecordsConsoleState : BoundUserInterfaceState
/// Currently selected crewmember record key.
/// </summary>
public uint? SelectedKey = null;
public CriminalRecord? CriminalRecord = null;
public GeneralStationRecord? StationRecord = null;
public SecurityStatus FilterStatus = SecurityStatus.None;
public readonly Dictionary<uint, string>? RecordListing;
public readonly StationRecordsFilter? Filter;
@@ -100,3 +100,20 @@ public sealed class CriminalRecordDeleteHistory : BoundUserInterfaceMessage
Index = index;
}
}
/// <summary>
/// Used to set what status to filter by index.
///
/// </summary>
///
[Serializable, NetSerializable]
public sealed class CriminalRecordSetStatusFilter : BoundUserInterfaceMessage
{
public readonly SecurityStatus FilterStatus;
public CriminalRecordSetStatusFilter(SecurityStatus newFilterStatus)
{
FilterStatus = newFilterStatus;
}
}

View File

@@ -24,6 +24,7 @@ public sealed partial class DisposalDoAfterEvent : SimpleDoAfterEvent
public abstract class SharedDisposalUnitSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] protected readonly EmagSystem _emag = default!;
[Dependency] protected readonly MetaDataSystem Metadata = default!;
[Dependency] protected readonly SharedJointSystem Joints = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
@@ -102,6 +103,12 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
protected void OnEmagged(EntityUid uid, SharedDisposalUnitComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (component.DisablePressure == true)
return;
component.DisablePressure = true;
args.Handled = true;
}

View File

@@ -34,6 +34,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] protected readonly TagSystem Tags = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
@@ -77,8 +78,6 @@ public abstract partial class SharedDoorSystem : EntitySystem
SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
SubscribeLocalEvent<DoorComponent, OnAttemptEmagEvent>(OnAttemptEmag);
SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
}
@@ -118,31 +117,24 @@ public abstract partial class SharedDoorSystem : EntitySystem
_activeDoors.Remove(door);
}
private void OnAttemptEmag(EntityUid uid, DoorComponent door, ref OnAttemptEmagEvent args)
{
if (!TryComp<AirlockComponent>(uid, out var airlock))
{
args.Handled = true;
return;
}
if (IsBolted(uid) || !airlock.Powered)
{
args.Handled = true;
return;
}
if (door.State != DoorState.Closed)
{
args.Handled = true;
}
}
private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!TryComp<AirlockComponent>(uid, out var airlock))
return;
if (IsBolted(uid) || !airlock.Powered)
return;
if (door.State != DoorState.Closed)
return;
if (!SetState(uid, DoorState.Emagging, door))
return;
Audio.PlayPredicted(door.SparkSound, uid, args.UserUid, AudioParams.Default.WithVolume(8));
args.Repeatable = true;
args.Handled = true;
}

View File

@@ -1,6 +1,8 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization;
@@ -14,7 +16,21 @@ public sealed partial class EmagComponent : Component
/// <summary>
/// The tag that marks an entity as immune to emags
/// </summary>
[DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>)), ViewVariables(VVAccess.ReadWrite)]
[DataField]
[AutoNetworkedField]
public string EmagImmuneTag = "EmagImmune";
public ProtoId<TagPrototype> EmagImmuneTag = "EmagImmune";
/// <summary>
/// What type of emag effect this device will do
/// </summary>
[DataField]
[AutoNetworkedField]
public EmagType EmagType = EmagType.Interaction;
/// <summary>
/// What sound should the emag play when used
/// </summary>
[DataField]
[AutoNetworkedField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.Emag.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Emag.Components;
@@ -5,7 +6,12 @@ namespace Content.Shared.Emag.Components;
/// <summary>
/// Marker component for emagged entities
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class EmaggedComponent : Component
{
/// <summary>
/// The EmagType flags that were used to emag this device
/// </summary>
[DataField, AutoNetworkedField]
public EmagType EmagType = EmagType.None;
}

View File

@@ -6,8 +6,9 @@ using Content.Shared.Emag.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Tag;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.Emag.Systems;
@@ -23,88 +24,124 @@ public sealed class EmagSystem : EntitySystem
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmagComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<EmaggedComponent, OnAccessOverriderAccessUpdatedEvent>(OnAccessOverriderAccessUpdated);
}
private void OnAccessOverriderAccessUpdated(Entity<EmaggedComponent> entity, ref OnAccessOverriderAccessUpdatedEvent args)
{
if (!CompareFlag(entity.Comp.EmagType, EmagType.Access))
return;
entity.Comp.EmagType &= ~EmagType.Access;
Dirty(entity);
}
private void OnAfterInteract(EntityUid uid, EmagComponent comp, AfterInteractEvent args)
{
if (!args.CanReach || args.Target is not { } target)
return;
args.Handled = TryUseEmag(uid, args.User, target, comp);
}
/// <summary>
/// Tries to use the emag on a target entity
/// </summary>
public bool TryUseEmag(EntityUid uid, EntityUid user, EntityUid target, EmagComponent? comp = null)
{
if (!Resolve(uid, ref comp, false))
return false;
if (_tag.HasTag(target, comp.EmagImmuneTag))
return false;
TryComp<LimitedChargesComponent>(uid, out var charges);
if (_charges.IsEmpty(uid, charges))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
}
var handled = DoEmagEffect(user, target);
if (!handled)
return false;
_popup.PopupClient(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
user, PopupType.Medium);
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}");
if (charges != null)
_charges.UseCharge(uid, charges);
return true;
args.Handled = TryEmagEffect((uid, comp), args.User, target);
}
/// <summary>
/// Does the emag effect on a specified entity
/// </summary>
public bool DoEmagEffect(EntityUid user, EntityUid target)
public bool TryEmagEffect(Entity<EmagComponent?> ent, EntityUid user, EntityUid target)
{
// prevent emagging twice
if (HasComp<EmaggedComponent>(target))
if (!Resolve(ent, ref ent.Comp, false))
return false;
var onAttemptEmagEvent = new OnAttemptEmagEvent(user);
RaiseLocalEvent(target, ref onAttemptEmagEvent);
// prevent emagging if attempt fails
if (onAttemptEmagEvent.Handled)
if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
return false;
var emaggedEvent = new GotEmaggedEvent(user);
TryComp<LimitedChargesComponent>(ent, out var charges);
if (_charges.IsEmpty(ent, charges))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
}
var emaggedEvent = new GotEmaggedEvent(user, ent.Comp.EmagType);
RaiseLocalEvent(target, ref emaggedEvent);
if (emaggedEvent.Handled && !emaggedEvent.Repeatable)
EnsureComp<EmaggedComponent>(target);
if (!emaggedEvent.Handled)
return false;
_popup.PopupPredicted(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.Medium);
_audio.PlayPredicted(ent.Comp.EmagSound, ent, ent);
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
if (charges != null && emaggedEvent.Handled)
_charges.UseCharge(ent, charges);
if (!emaggedEvent.Repeatable)
{
EnsureComp<EmaggedComponent>(target, out var emaggedComp);
emaggedComp.EmagType |= ent.Comp.EmagType;
Dirty(target, emaggedComp);
}
return emaggedEvent.Handled;
}
/// <summary>
/// Checks whether an entity has the EmaggedComponent with a set flag.
/// </summary>
/// <param name="target">The target entity to check for the flag.</param>
/// <param name="flag">The EmagType flag to check for.</param>
/// <returns>True if entity has EmaggedComponent and the provided flag. False if the entity lacks EmaggedComponent or provided flag.</returns>
public bool CheckFlag(EntityUid target, EmagType flag)
{
if (!TryComp<EmaggedComponent>(target, out var comp))
return false;
if ((comp.EmagType & flag) == flag)
return true;
return false;
}
/// <summary>
/// Compares a flag to the target.
/// </summary>
/// <param name="target">The target flag to check.</param>
/// <param name="flag">The flag to check for within the target.</param>
/// <returns>True if target contains flag. Otherwise false.</returns>
public bool CompareFlag(EmagType target, EmagType flag)
{
if ((target & flag) == flag)
return true;
return false;
}
}
[Flags]
[Serializable, NetSerializable]
public enum EmagType : byte
{
None = 0,
Interaction = 1 << 1,
Access = 1 << 2
}
/// <summary>
/// Shows a popup to emag user (client side only!) and adds <see cref="EmaggedComponent"/> to the entity when handled
/// </summary>
/// <param name="UserUid">Emag user</param>
/// <param name="Type">The emag type to use</param>
/// <param name="Handled">Did the emagging succeed? Causes a user-only popup to show on client side</param>
/// <param name="Repeatable">Can the entity be emagged more than once? Prevents adding of <see cref="EmaggedComponent"/></param>
/// <remarks>Needs to be handled in shared/client, not just the server, to actually show the emagging popup</remarks>
[ByRefEvent]
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
[ByRefEvent]
public record struct OnAttemptEmagEvent(EntityUid UserUid, bool Handled = false);
public record struct GotEmaggedEvent(EntityUid UserUid, EmagType Type, bool Handled = false, bool Repeatable = false);

View File

@@ -59,12 +59,6 @@ public sealed partial class FaxMachineComponent : Component
[DataField]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Sound to play when fax printing new message
/// </summary>

View File

@@ -21,10 +21,4 @@ public sealed partial class OrbitVisualsComponent : Component
/// How long should the orbit stop animation last in seconds?
/// </summary>
public float OrbitStopLength = 1.0f;
/// <summary>
/// How far along in the orbit, from 0 to 1, is this entity?
/// </summary>
[Animatable]
public float Orbit { get; set; } = 0.0f;
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Gives the user access to a given channel without the need for a headset.
/// </summary>
[RegisterComponent]
public sealed partial class RadioImplantComponent : Component
{
/// <summary>
/// The radio channel(s) to grant access to.
/// </summary>
[DataField(required: true)]
public HashSet<ProtoId<RadioChannelPrototype>> RadioChannels = new();
/// <summary>
/// The radio channels that have been added by the implant to a user's ActiveRadioComponent.
/// Used to track which channels were successfully added (not already in user)
/// </summary>
/// <remarks>
/// Should not be modified outside RadioImplantSystem.cs
/// </remarks>
[DataField]
public HashSet<ProtoId<RadioChannelPrototype>> ActiveAddedChannels = new();
/// <summary>
/// The radio channels that have been added by the implant to a user's IntrinsicRadioTransmitterComponent.
/// Used to track which channels were successfully added (not already in user)
/// </summary>
/// <remarks>
/// Should not be modified outside RadioImplantSystem.cs
/// </remarks>
[DataField]
public HashSet<ProtoId<RadioChannelPrototype>> TransmitterAddedChannels = new();
}

View File

@@ -1,13 +1,10 @@
namespace Content.Shared.Inventory.Events;
public sealed class RefreshEquipmentHudEvent<T> : EntityEventArgs, IInventoryRelayEvent where T : IComponent
[ByRefEvent]
public record struct RefreshEquipmentHudEvent<T>(SlotFlags TargetSlots) : IInventoryRelayEvent
where T : IComponent
{
public SlotFlags TargetSlots { get; init; }
public SlotFlags TargetSlots { get; } = TargetSlots;
public bool Active = false;
public List<T> Components = new();
public RefreshEquipmentHudEvent(SlotFlags targetSlots)
{
TargetSlots = targetSlots;
}
}

View File

@@ -55,14 +55,14 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, SolutionScanEvent>(RelayInventoryEvent);
// ComponentActivatedClientSystems
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowJobIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthBarsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHungerIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowThirstIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowMindShieldIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSyndicateIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowJobIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthBarsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHungerIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowThirstIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowMindShieldIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSyndicateIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
}

View File

@@ -18,6 +18,7 @@ public abstract class SharedLatheSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedMaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
@@ -66,6 +67,12 @@ public abstract class SharedLatheSystem : EntitySystem
private void OnEmagged(EntityUid uid, EmagLatheRecipesComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Actions;
using Content.Shared.Emag.Systems;
using Content.Shared.Light.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Storage.Components;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
@@ -22,6 +23,7 @@ public sealed class UnpoweredFlashlightSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize()
{
@@ -78,6 +80,9 @@ public sealed class UnpoweredFlashlightSystem : EntitySystem
private void OnGotEmagged(EntityUid uid, UnpoweredFlashlightComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (!_light.TryGetLight(uid, out var light))
return;

View File

@@ -54,9 +54,9 @@ public sealed partial class LockComponent : Component
/// <summary>
/// Whether or not an emag disables it.
/// </summary>
[DataField("breakOnEmag")]
[DataField]
[AutoNetworkedField]
public bool BreakOnEmag = true;
public bool BreakOnAccessBreaker = true;
/// <summary>
/// Amount of do-after time needed to lock the entity.

View File

@@ -28,6 +28,7 @@ public sealed class LockSystem : EntitySystem
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!;
@@ -295,7 +296,10 @@ public sealed class LockSystem : EntitySystem
private void OnEmagged(EntityUid uid, LockComponent component, ref GotEmaggedEvent args)
{
if (!component.Locked || !component.BreakOnEmag)
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!component.Locked || !component.BreakOnAccessBreaker)
return;
_audio.PlayPredicted(component.UnlockSound, uid, args.UserUid);
@@ -307,7 +311,7 @@ public sealed class LockSystem : EntitySystem
var ev = new LockToggledEvent(false);
RaiseLocalEvent(uid, ref ev, true);
RemComp<LockComponent>(uid); //Literally destroys the lock as a tell it was emagged
args.Repeatable = true;
args.Handled = true;
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Actions;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Magic.Events;
public sealed partial class VoidApplauseSpellEvent : EntityTargetActionEvent, ISpeakSpell
{
[DataField]
public string? Speech { get; private set; }
/// <summary>
/// Emote to use.
/// </summary>
[DataField]
public ProtoId<EmotePrototype> Emote = "ClapSingle";
/// <summary>
/// Visual effect entity that is spawned at both the user's and the target's location.
/// </summary>
[DataField]
public EntProtoId Effect = "EffectVoidBlink";
}

View File

@@ -7,7 +7,6 @@ using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Lock;
@@ -15,9 +14,6 @@ using Content.Shared.Magic.Components;
using Content.Shared.Magic.Events;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
@@ -37,6 +33,10 @@ using Robust.Shared.Spawners;
namespace Content.Shared.Magic;
// TODO: Move BeforeCast & Prerequirements (like Wizard clothes) to action comp
// Alt idea - make it its own comp and split, like the Charge PR
// TODO: Move speech to actionComp or again, its own ECS
// TODO: Use the MagicComp just for pure backend things like spawning patterns?
/// <summary>
/// Handles learning and using spells (actions)
/// </summary>
@@ -60,7 +60,6 @@ public abstract class SharedMagicSystem : EntitySystem
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
@@ -80,79 +79,7 @@ public abstract class SharedMagicSystem : EntitySystem
SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell);
// Spell wishlist
// A wishlish of spells that I'd like to implement or planning on implementing in a future PR
// TODO: InstantDoAfterSpell and WorldDoafterSpell
// Both would be an action that take in an event, that passes an event to trigger once the doafter is done
// This would be three events:
// 1 - Event that triggers from the action that starts the doafter
// 2 - The doafter event itself, which passes the event with it
// 3 - The event to trigger once the do-after finishes
// TODO: Inanimate objects to life ECS
// AI sentience
// TODO: Flesh2Stone
// Entity Target spell
// Synergy with Inanimate object to life (detects player and allows player to move around)
// TODO: Lightning Spell
// Should just fire lightning, try to prevent arc back to caster
// TODO: Magic Missile (homing projectile ecs)
// Instant action, target any player (except self) on screen
// TODO: Random projectile ECS for magic-carp, wand of magic
// TODO: Recall Spell
// mark any item in hand to recall
// ItemRecallComponent
// Event adds the component if it doesn't exist and the performer isn't stored in the comp
// 2nd firing of the event checks to see if the recall comp has this uid, and if it does it calls it
// if no free hands, summon at feet
// if item deleted, clear stored item
// TODO: Jaunt (should be its own ECS)
// Instant action
// When clicked, disappear/reappear (goes to paused map)
// option to restrict to tiles
// option for requiring entry/exit (blood jaunt)
// speed option
// TODO: Summon Events
// List of wizard events to add into the event pool that frequently activate
// floor is lava
// change places
// ECS that when triggered, will periodically trigger a random GameRule
// Would need a controller/controller entity?
// TODO: Summon Guns
// Summon a random gun at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving gun collector status to people
// TODO: Summon Magic
// Summon a random magic wand at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving magic collector status to people
// TODO: Bottle of Blood
// Summons Slaughter Demon
// TODO: Slaughter Demon
// Also see Jaunt
// TODO: Field Spells
// Should be able to specify a grid of tiles (3x3 for example) that it effects
// Timed despawn - so it doesn't last forever
// Ignore caster - for spells that shouldn't effect the caster (ie if timestop should effect the caster)
// TODO: Touch toggle spell
// 1 - When toggled on, show in hand
// 2 - Block hand when toggled on
// - Require free hand
// 3 - use spell event when toggled & click
SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause);
}
private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
@@ -402,8 +329,7 @@ public abstract class SharedMagicSystem : EntitySystem
return;
var transform = Transform(args.Performer);
if (transform.MapID != args.Target.GetMapId(EntityManager) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
if (transform.MapID != _transform.GetMapId(args.Target) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
return;
_transform.SetCoordinates(args.Performer, args.Target);
@@ -411,6 +337,17 @@ public abstract class SharedMagicSystem : EntitySystem
Speak(args);
args.Handled = true;
}
public virtual void OnVoidApplause(VoidApplauseSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
_transform.SwapPositions(ev.Performer, ev.Target);
}
// End Teleport Spells
#endregion
#region Spell Helpers
@@ -435,7 +372,7 @@ public abstract class SharedMagicSystem : EntitySystem
}
// End Spell Helpers
#endregion
#region Smite Spells
#region Touch Spells
private void OnSmiteSpell(SmiteSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
@@ -454,7 +391,8 @@ public abstract class SharedMagicSystem : EntitySystem
_body.GibBody(ev.Target, true, body);
}
// End Smite Spells
// End Touch Spells
#endregion
#region Knock Spells
/// <summary>

View File

@@ -29,6 +29,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedContainerSystem Container = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public const string ActiveReclaimerContainerId = "active-material-reclaimer-container";
@@ -60,6 +61,12 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
private void OnEmagged(EntityUid uid, MaterialReclaimerComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}
@@ -207,7 +214,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
component.Enabled &&
!component.Broken &&
HasComp<BodyComponent>(victim) &&
HasComp<EmaggedComponent>(uid);
_emag.CheckFlag(uid, EmagType.Interaction);
}
/// <summary>

View File

@@ -20,6 +20,7 @@ public abstract partial class SharedCryoPodSystem: EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly StandingStateSystem _standingStateSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -156,9 +157,13 @@ public abstract partial class SharedCryoPodSystem: EntitySystem
protected void OnEmagged(EntityUid uid, CryoPodComponent? cryoPodComponent, ref GotEmaggedEvent args)
{
if (!Resolve(uid, ref cryoPodComponent))
{
return;
}
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (cryoPodComponent.PermaLocked && cryoPodComponent.Locked)
return;
cryoPodComponent.PermaLocked = true;
cryoPodComponent.Locked = true;

View File

@@ -2,6 +2,7 @@
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Robust.Shared.Containers;
namespace Content.Shared.Mindshield.FakeMindShield;
@@ -13,7 +14,9 @@ public sealed class SharedFakeMindShieldImplantSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, FakeMindShieldToggleEvent>(OnFakeMindShieldToggle);
SubscribeLocalEvent<FakeMindShieldImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<FakeMindShieldImplantComponent, EntGotRemovedFromContainerMessage>(ImplantDraw);
}
/// <summary>
/// Raise the Action of a Implanted user toggling their implant to the FakeMindshieldComponent on their entity
/// </summary>
@@ -33,4 +36,9 @@ public sealed class SharedFakeMindShieldImplantSystem : EntitySystem
if (ev.Implanted != null)
EnsureComp<FakeMindShieldComponent>(ev.Implanted.Value);
}
private void ImplantDraw(Entity<FakeMindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage ev)
{
RemComp<FakeMindShieldComponent>(ev.Container.Owner);
}
}

View File

@@ -1,8 +1,10 @@
namespace Content.Server.NPC.Components
using Robust.Shared.GameStates;
namespace Content.Shared.NPC.Components
{
/// Added when a medibot injects someone
/// So they don't get injected again for at least a minute.
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class NPCRecentlyInjectedComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("accumulator")]

View File

@@ -1,6 +1,8 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Ninja.Systems;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -17,11 +19,23 @@ public sealed partial class EmagProviderComponent : Component
/// The tag that marks an entity as immune to emagging.
/// </summary>
[DataField]
public ProtoId<TagPrototype> EmagImmuneTag = "EmagImmune";
public ProtoId<TagPrototype> AccessBreakerImmuneTag = "AccessBreakerImmune";
/// <summary>
/// Whitelist that entities must be on to work.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// What type of emag this will provide.
/// </summary>
[DataField]
public EmagType EmagType = EmagType.Access;
/// <summary>
/// What sound should the emag play when used
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
}

View File

@@ -5,6 +5,7 @@ using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
namespace Content.Shared.Ninja.Systems;
@@ -13,6 +14,7 @@ namespace Content.Shared.Ninja.Systems;
/// </summary>
public sealed class EmagProviderSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@@ -42,14 +44,18 @@ public sealed class EmagProviderSystem : EntitySystem
return;
// only allowed to emag non-immune entities
if (_tag.HasTag(target, comp.EmagImmuneTag))
if (_tag.HasTag(target, comp.AccessBreakerImmuneTag))
return;
var handled = _emag.DoEmagEffect(uid, target);
if (!handled)
var emagEv = new GotEmaggedEvent(uid, EmagType.Access);
RaiseLocalEvent(args.Target, ref emagEv);
if (!emagEv.Handled)
return;
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(uid):player} emagged {ToPrettyString(target):target}");
_audio.PlayPredicted(comp.EmagSound, uid, uid);
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(uid):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
var ev = new EmaggedSomethingEvent(target);
RaiseLocalEvent(uid, ref ev);
args.Handled = true;
@@ -57,7 +63,7 @@ public sealed class EmagProviderSystem : EntitySystem
}
/// <summary>
/// Raised on the player when emagging something.
/// Raised on the player when access breaking something.
/// </summary>
[ByRefEvent]
public record struct EmaggedSomethingEvent(EntityUid Target);

View File

@@ -10,6 +10,7 @@ namespace Content.Shared.Pinpointer;
public abstract class SharedPinpointerSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize()
{
@@ -137,6 +138,15 @@ public abstract class SharedPinpointerSystem : EntitySystem
private void OnEmagged(EntityUid uid, PinpointerComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (component.CanRetarget)
return;
args.Handled = true;
component.CanRetarget = true;
}

View File

@@ -40,13 +40,16 @@ public sealed partial class RoleTimeRequirement : JobRequirement
var formattedRoleDiff = ContentLocalizationManager.FormatPlaytime(roleDiffSpan);
var departmentColor = Color.Yellow;
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
{
if (!entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
return false;
var jobProto = jobSystem.GetJobPrototype(proto);
if (jobSystem.TryGetDepartment(jobProto, out var departmentProto))
departmentColor = departmentProto.Color;
}
if (!protoManager.TryIndex<JobPrototype>(jobProto, out var indexedJob))
return false;
if (!Inverted)
{
@@ -56,7 +59,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("job", indexedJob.LocalizedName),
("departmentColor", departmentColor.ToHex())));
return false;
}
@@ -66,7 +69,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("job", indexedJob.LocalizedName),
("departmentColor", departmentColor.ToHex())));
return false;
}

View File

@@ -16,13 +16,4 @@ public sealed partial class EmaggableMedibotComponent : Component
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public Dictionary<MobState, MedibotTreatment> Replacements = new();
/// <summary>
/// Sound to play when the bot has been emagged
/// </summary>
[DataField]
public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks")
{
Params = AudioParams.Default.WithVolume(8f)
};
}

View File

@@ -1,6 +1,15 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Interaction;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.Bots;
@@ -11,20 +20,31 @@ namespace Content.Shared.Silicons.Bots;
public sealed class MedibotSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private SharedInteractionSystem _interaction = default!;
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private SharedDoAfterSystem _doAfter = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmaggableMedibotComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<MedibotComponent, UserActivateInWorldEvent>(OnInteract);
SubscribeLocalEvent<MedibotComponent, MedibotInjectDoAfterEvent>(OnInject);
}
private void OnEmagged(EntityUid uid, EmaggableMedibotComponent comp, ref GotEmaggedEvent args)
{
if (!TryComp<MedibotComponent>(uid, out var medibot))
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
_audio.PlayPredicted(comp.SparkSound, uid, args.UserUid);
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (!TryComp<MedibotComponent>(uid, out var medibot))
return;
foreach (var (state, treatment) in comp.Replacements)
{
@@ -34,6 +54,25 @@ public sealed class MedibotSystem : EntitySystem
args.Handled = true;
}
private void OnInteract(Entity<MedibotComponent> medibot, ref UserActivateInWorldEvent args)
{
if (!CheckInjectable(medibot!, args.Target, true)) return;
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, 2f, new MedibotInjectDoAfterEvent(), args.User, args.Target)
{
BlockDuplicate = true,
BreakOnMove = true,
});
}
private void OnInject(EntityUid uid, MedibotComponent comp, ref MedibotInjectDoAfterEvent args)
{
if (args.Cancelled) return;
if (args.Target is { } target)
TryInject(uid, target);
}
/// <summary>
/// Get a treatment for a given mob state.
/// </summary>
@@ -44,4 +83,66 @@ public sealed class MedibotSystem : EntitySystem
{
return comp.Treatments.TryGetValue(state, out treatment);
}
/// <summary>
/// Checks if the target can be injected.
/// </summary>
public bool CheckInjectable(Entity<MedibotComponent?> medibot, EntityUid target, bool manual = false)
{
if (!Resolve(medibot, ref medibot.Comp, false)) return false;
if (HasComp<NPCRecentlyInjectedComponent>(target))
{
_popup.PopupClient(Loc.GetString("medibot-recently-injected"), medibot, medibot);
return false;
}
if (!TryComp<MobStateComponent>(target, out var mobState)) return false;
if (!TryComp<DamageableComponent>(target, out var damageable)) return false;
if (!_solutionContainer.TryGetInjectableSolution(target, out _, out _)) return false;
if (mobState.CurrentState != MobState.Alive && mobState.CurrentState != MobState.Critical)
{
_popup.PopupClient(Loc.GetString("medibot-target-dead"), medibot, medibot);
return false;
}
var total = damageable.TotalDamage;
if (total == 0 && !HasComp<EmaggedComponent>(medibot))
{
_popup.PopupClient(Loc.GetString("medibot-target-healthy"), medibot, medibot);
return false;
}
if (!TryGetTreatment(medibot.Comp, mobState.CurrentState, out var treatment) || !treatment.IsValid(total) && !manual) return false;
return true;
}
/// <summary>
/// Tries to inject the target.
/// </summary>
public bool TryInject(Entity<MedibotComponent?> medibot, EntityUid target)
{
if (!Resolve(medibot, ref medibot.Comp, false)) return false;
if (!_interaction.InRangeUnobstructed(medibot.Owner, target)) return false;
if (!TryComp<MobStateComponent>(target, out var mobState)) return false;
if (!TryGetTreatment(medibot.Comp, mobState.CurrentState, out var treatment)) return false;
if (!_solutionContainer.TryGetInjectableSolution(target, out var injectable, out _)) return false;
EnsureComp<NPCRecentlyInjectedComponent>(target);
_solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _);
_popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target);
_popup.PopupClient(Loc.GetString("medibot-target-injected"), medibot, medibot);
_audio.PlayPredicted(medibot.Comp.InjectSound, medibot, medibot);
return true;
}
}
[Serializable, NetSerializable]
public sealed partial class MedibotInjectDoAfterEvent : SimpleDoAfterEvent { }

View File

@@ -1,7 +1,10 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Mind;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
using Content.Shared.Wires;
using Robust.Shared.Audio;
namespace Content.Shared.Silicons.Laws;
@@ -11,22 +14,29 @@ namespace Content.Shared.Silicons.Laws;
public abstract partial class SharedSiliconLawSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
/// <inheritdoc/>
public override void Initialize()
{
InitializeUpdater();
SubscribeLocalEvent<EmagSiliconLawComponent, GotEmaggedEvent>(OnGotEmagged);
SubscribeLocalEvent<EmagSiliconLawComponent, OnAttemptEmagEvent>(OnAttemptEmag);
}
protected virtual void OnAttemptEmag(EntityUid uid, EmagSiliconLawComponent component, ref OnAttemptEmagEvent args)
private void OnGotEmagged(EntityUid uid, EmagSiliconLawComponent component, ref GotEmaggedEvent args)
{
//prevent self emagging
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
// prevent self-emagging
if (uid == args.UserUid)
{
_popup.PopupClient(Loc.GetString("law-emag-cannot-emag-self"), uid, args.UserUid);
args.Handled = true;
return;
}
@@ -35,14 +45,33 @@ public abstract partial class SharedSiliconLawSystem : EntitySystem
!panel.Open)
{
_popup.PopupClient(Loc.GetString("law-emag-require-panel"), uid, args.UserUid);
args.Handled = true;
return;
}
}
var ev = new SiliconEmaggedEvent(args.UserUid);
RaiseLocalEvent(uid, ref ev);
protected virtual void OnGotEmagged(EntityUid uid, EmagSiliconLawComponent component, ref GotEmaggedEvent args)
{
component.OwnerName = Name(args.UserUid);
NotifyLawsChanged(uid, component.EmaggedSound);
if(_mind.TryGetMind(uid, out var mindId, out _))
EnsureSubvertedSiliconRole(mindId);
_stunSystem.TryParalyze(uid, component.StunTime, true);
args.Handled = true;
}
protected virtual void NotifyLawsChanged(EntityUid uid, SoundSpecifier? cue = null)
{
}
protected virtual void EnsureSubvertedSiliconRole(EntityUid mindId)
{
}
}
[ByRefEvent]
public record struct SiliconEmaggedEvent(EntityUid user);

View File

@@ -11,6 +11,7 @@ public abstract class SharedSingularityGeneratorSystem : EntitySystem
{
#region Dependencies
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
#endregion Dependencies
public override void Initialize()
@@ -22,6 +23,15 @@ public abstract class SharedSingularityGeneratorSystem : EntitySystem
private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (component.FailsafeDisabled)
return;
component.FailsafeDisabled = true;
args.Handled = true;
}

View File

@@ -2,7 +2,7 @@ using Content.Shared.Stunnable;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Server.Stunnable.Components;
namespace Content.Shared.Stunnable;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]

View File

@@ -2,6 +2,7 @@ using Content.Shared.Emag.Components;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Systems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Audio;
@@ -19,11 +20,14 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] protected readonly IRobustRandom Randomizer = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
}
@@ -49,9 +53,21 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
Dirty(uid, component);
}
private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
// only emag if there are emag-only items
args.Handled = component.EmaggedInventory.Count > 0;
}
/// <summary>
/// Returns all of the vending machine's inventory. Only includes emagged and contraband inventories if
/// <see cref="EmaggedComponent"/> exists and <see cref="VendingMachineComponent.Contraband"/> is true
/// <see cref="EmaggedComponent"/> with the EmagType.Interaction flag exists and <see cref="VendingMachineComponent.Contraband"/> is true
/// are <c>true</c> respectively.
/// </summary>
/// <param name="uid"></param>
@@ -64,7 +80,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
var inventory = new List<VendingMachineInventoryEntry>(component.Inventory.Values);
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
inventory.AddRange(component.EmaggedInventory.Values);
if (component.Contraband)

View File

@@ -508,7 +508,7 @@ public abstract partial class SharedGunSystem : EntitySystem
return;
var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle);
CreateEffect(gun, ev, gun);
CreateEffect(gun, ev, user);
}
public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)

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