mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 01:15:13 +01:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # .github/CODEOWNERS # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Maps/core.yml # Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml # Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml # Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml # Resources/Prototypes/Roles/Jobs/Civilian/musician.yml # Resources/Prototypes/Roles/Jobs/Command/centcom_official.yml # Resources/Textures/Clothing/Hands/Gloves/captain.rsi/equipped-HAND.png # Resources/Textures/Clothing/Hands/Gloves/captain.rsi/icon.png # Resources/Textures/Clothing/Hands/Gloves/captain.rsi/meta.json # Resources/Textures/Clothing/Head/Hats/capcap.rsi/equipped-HELMET.png # Resources/Textures/Clothing/Head/Hats/capcap.rsi/icon.png # Resources/Textures/Clothing/Head/Hats/capcap.rsi/meta.json # Resources/Textures/Clothing/Head/Hats/captain.rsi/equipped-HELMET.png # Resources/Textures/Clothing/Head/Hats/captain.rsi/icon.png # Resources/Textures/Clothing/Head/Hats/captain.rsi/meta.json # Resources/Textures/Clothing/Neck/Cloaks/cap.rsi/equipped-NECK.png # Resources/Textures/Clothing/Neck/Cloaks/cap.rsi/icon.png # Resources/Textures/Clothing/Neck/Cloaks/cap.rsi/inhand-left.png # Resources/Textures/Clothing/Neck/Cloaks/cap.rsi/inhand-right.png # Resources/Textures/Clothing/Neck/Cloaks/cap.rsi/meta.json # Resources/Textures/Clothing/Neck/Cloaks/capcloakformal.rsi/equipped-NECK.png # Resources/Textures/Clothing/Neck/Cloaks/capcloakformal.rsi/icon.png # Resources/Textures/Clothing/Neck/Cloaks/capcloakformal.rsi/meta.json # Resources/Textures/Clothing/Neck/mantles/capmantle.rsi/equipped-NECK.png # Resources/Textures/Clothing/Neck/mantles/capmantle.rsi/icon.png # Resources/Textures/Clothing/Neck/mantles/capmantle.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Armor/captain_carapace.rsi/equipped-OUTERCLOTHING.png # Resources/Textures/Clothing/OuterClothing/Armor/captain_carapace.rsi/icon.png # Resources/Textures/Clothing/OuterClothing/Armor/captain_carapace.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcap.rsi/equipped-OUTERCLOTHING.png # Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcap.rsi/icon.png # Resources/Textures/Clothing/OuterClothing/WinterCoats/coatcap.rsi/meta.json # Resources/Textures/Clothing/Uniforms/Jumpskirt/captain.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpskirt/captain.rsi/icon.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/captain.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/captain.rsi/icon.png # Resources/Textures/Decals/bricktile.rsi/dark_box.png # Resources/Textures/Decals/bricktile.rsi/dark_corner_ne.png # Resources/Textures/Decals/bricktile.rsi/dark_corner_nw.png # Resources/Textures/Decals/bricktile.rsi/dark_corner_se.png # Resources/Textures/Decals/bricktile.rsi/dark_corner_sw.png # Resources/Textures/Decals/bricktile.rsi/dark_end_e.png # Resources/Textures/Decals/bricktile.rsi/dark_end_n.png # Resources/Textures/Decals/bricktile.rsi/dark_end_s.png # Resources/Textures/Decals/bricktile.rsi/dark_end_w.png # Resources/Textures/Decals/bricktile.rsi/dark_inner_ne.png # Resources/Textures/Decals/bricktile.rsi/dark_inner_nw.png # Resources/Textures/Decals/bricktile.rsi/dark_inner_se.png # Resources/Textures/Decals/bricktile.rsi/dark_inner_sw.png # Resources/Textures/Decals/bricktile.rsi/dark_line_e.png # Resources/Textures/Decals/bricktile.rsi/dark_line_n.png # Resources/Textures/Decals/bricktile.rsi/dark_line_s.png # Resources/Textures/Decals/bricktile.rsi/dark_line_w.png # Resources/Textures/Decals/bricktile.rsi/meta.json # Resources/Textures/Decals/bricktile.rsi/steel_box.png # Resources/Textures/Decals/bricktile.rsi/steel_corner_ne.png # Resources/Textures/Decals/bricktile.rsi/steel_corner_nw.png # Resources/Textures/Decals/bricktile.rsi/steel_corner_se.png # Resources/Textures/Decals/bricktile.rsi/steel_corner_sw.png # Resources/Textures/Decals/bricktile.rsi/steel_end_e.png # Resources/Textures/Decals/bricktile.rsi/steel_end_n.png # Resources/Textures/Decals/bricktile.rsi/steel_end_s.png # Resources/Textures/Decals/bricktile.rsi/steel_end_w.png # Resources/Textures/Decals/bricktile.rsi/steel_inner_ne.png # Resources/Textures/Decals/bricktile.rsi/steel_inner_nw.png # Resources/Textures/Decals/bricktile.rsi/steel_inner_se.png # Resources/Textures/Decals/bricktile.rsi/steel_inner_sw.png # Resources/Textures/Decals/bricktile.rsi/steel_line_e.png # Resources/Textures/Decals/bricktile.rsi/steel_line_n.png # Resources/Textures/Decals/bricktile.rsi/steel_line_w.png # Resources/Textures/Decals/bricktile.rsi/white_box.png # Resources/Textures/Decals/bricktile.rsi/white_corner_ne.png # Resources/Textures/Decals/bricktile.rsi/white_corner_nw.png # Resources/Textures/Decals/bricktile.rsi/white_corner_se.png # Resources/Textures/Decals/bricktile.rsi/white_corner_sw.png # Resources/Textures/Decals/bricktile.rsi/white_end_e.png # Resources/Textures/Decals/bricktile.rsi/white_end_n.png # Resources/Textures/Decals/bricktile.rsi/white_end_s.png # Resources/Textures/Decals/bricktile.rsi/white_end_w.png # Resources/Textures/Decals/bricktile.rsi/white_inner_ne.png # Resources/Textures/Decals/bricktile.rsi/white_inner_nw.png # Resources/Textures/Decals/bricktile.rsi/white_inner_se.png # Resources/Textures/Decals/bricktile.rsi/white_inner_sw.png # Resources/Textures/Decals/bricktile.rsi/white_line_e.png # Resources/Textures/Decals/bricktile.rsi/white_line_n.png # Resources/Textures/Decals/bricktile.rsi/white_line_s.png # Resources/Textures/Decals/bricktile.rsi/white_line_w.png # Resources/Textures/Decals/minitile.rsi/dark_box.png # Resources/Textures/Decals/minitile.rsi/dark_corner_ne.png # Resources/Textures/Decals/minitile.rsi/dark_corner_nw.png # Resources/Textures/Decals/minitile.rsi/dark_corner_se.png # Resources/Textures/Decals/minitile.rsi/dark_corner_sw.png # Resources/Textures/Decals/minitile.rsi/dark_end_e.png # Resources/Textures/Decals/minitile.rsi/dark_end_n.png # Resources/Textures/Decals/minitile.rsi/dark_end_s.png # Resources/Textures/Decals/minitile.rsi/dark_end_w.png # Resources/Textures/Decals/minitile.rsi/dark_inner_ne.png # Resources/Textures/Decals/minitile.rsi/dark_inner_nw.png # Resources/Textures/Decals/minitile.rsi/dark_inner_se.png # Resources/Textures/Decals/minitile.rsi/dark_inner_sw.png # Resources/Textures/Decals/minitile.rsi/dark_line_e.png # Resources/Textures/Decals/minitile.rsi/dark_line_n.png # Resources/Textures/Decals/minitile.rsi/dark_line_s.png # Resources/Textures/Decals/minitile.rsi/dark_line_w.png # Resources/Textures/Decals/minitile.rsi/meta.json # Resources/Textures/Decals/minitile.rsi/steel_corner_ne.png # Resources/Textures/Decals/minitile.rsi/steel_corner_nw.png # Resources/Textures/Decals/minitile.rsi/steel_corner_se.png # Resources/Textures/Decals/minitile.rsi/steel_corner_sw.png # Resources/Textures/Decals/minitile.rsi/steel_end_e.png # Resources/Textures/Decals/minitile.rsi/steel_end_n.png # Resources/Textures/Decals/minitile.rsi/steel_end_s.png # Resources/Textures/Decals/minitile.rsi/steel_end_w.png # Resources/Textures/Decals/minitile.rsi/steel_inner_ne.png # Resources/Textures/Decals/minitile.rsi/steel_inner_nw.png # Resources/Textures/Decals/minitile.rsi/steel_inner_se.png # Resources/Textures/Decals/minitile.rsi/steel_inner_sw.png # Resources/Textures/Decals/minitile.rsi/steel_line_e.png # Resources/Textures/Decals/minitile.rsi/steel_line_n.png # Resources/Textures/Decals/minitile.rsi/steel_line_s.png # Resources/Textures/Decals/minitile.rsi/steel_line_w.png # Resources/Textures/Decals/minitile.rsi/white_box.png # Resources/Textures/Decals/minitile.rsi/white_corner_ne.png # Resources/Textures/Decals/minitile.rsi/white_corner_nw.png # Resources/Textures/Decals/minitile.rsi/white_corner_se.png # Resources/Textures/Decals/minitile.rsi/white_corner_sw.png # Resources/Textures/Decals/minitile.rsi/white_end_e.png # Resources/Textures/Decals/minitile.rsi/white_end_n.png # Resources/Textures/Decals/minitile.rsi/white_end_s.png # Resources/Textures/Decals/minitile.rsi/white_end_w.png # Resources/Textures/Decals/minitile.rsi/white_inner_ne.png # Resources/Textures/Decals/minitile.rsi/white_inner_nw.png # Resources/Textures/Decals/minitile.rsi/white_inner_se.png # Resources/Textures/Decals/minitile.rsi/white_inner_sw.png # Resources/Textures/Decals/minitile.rsi/white_line_w.png # Resources/Textures/Tiles/attributions.yml # Resources/Textures/Tiles/bar.png # Resources/Textures/Tiles/clown.png # Resources/Textures/Tiles/dark.png # Resources/Textures/Tiles/dark_diagonal.png # Resources/Textures/Tiles/dark_diagonal_mini.png # Resources/Textures/Tiles/dark_herringbone.png # Resources/Textures/Tiles/dark_mini.png # Resources/Textures/Tiles/dark_mono.png # Resources/Textures/Tiles/dark_offset.png # Resources/Textures/Tiles/dark_pavement.png # Resources/Textures/Tiles/dark_pavement_vertical.png # Resources/Textures/Tiles/hydro.png # Resources/Textures/Tiles/kitchen.png # Resources/Textures/Tiles/laundry.png # Resources/Textures/Tiles/steel.png # Resources/Textures/Tiles/steel_diagonal.png # Resources/Textures/Tiles/steel_diagonal_mini.png # Resources/Textures/Tiles/steel_herringbone.png # Resources/Textures/Tiles/steel_mini.png # Resources/Textures/Tiles/steel_mono.png # Resources/Textures/Tiles/steel_offset.png # Resources/Textures/Tiles/steel_pavement.png # Resources/Textures/Tiles/steel_pavement_vertical.png # Resources/Textures/Tiles/white.png # Resources/Textures/Tiles/white_diagonal.png # Resources/Textures/Tiles/white_diagonal_mini.png # Resources/Textures/Tiles/white_herringbone.png # Resources/Textures/Tiles/white_mini.png # Resources/Textures/Tiles/white_mono.png # Resources/Textures/Tiles/white_offset.png # Resources/Textures/Tiles/white_pavement.png # Resources/Textures/Tiles/white_pavement_vertical.png
This commit is contained in:
@@ -18,7 +18,8 @@ namespace Content.Client.Access.UI
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new AgentIDCardWindow();
|
||||
_window?.Dispose();
|
||||
_window = new AgentIDCardWindow(this);
|
||||
if (State != null)
|
||||
UpdateState(State);
|
||||
|
||||
@@ -39,6 +40,11 @@ namespace Content.Client.Access.UI
|
||||
SendMessage(new AgentIDCardJobChangedMessage(newJob));
|
||||
}
|
||||
|
||||
public void OnJobIconChanged(string newJobIcon)
|
||||
{
|
||||
SendMessage(new AgentIDCardJobIconChangedMessage(newJobIcon));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the UI state based on server-sent info
|
||||
/// </summary>
|
||||
@@ -51,6 +57,7 @@ namespace Content.Client.Access.UI
|
||||
|
||||
_window.SetCurrentName(cast.CurrentName);
|
||||
_window.SetCurrentJob(cast.CurrentJob);
|
||||
_window.SetAllowedIcons(cast.Icons);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -6,5 +6,12 @@
|
||||
<LineEdit Name="NameLineEdit" />
|
||||
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
|
||||
<LineEdit Name="JobLineEdit" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
|
||||
<Control HorizontalExpand="True" MinSize="50 0"/>
|
||||
<GridContainer Name="IconGrid" Columns="10">
|
||||
<!-- Job icon buttons are generated in the code -->
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Access.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AgentIDCardWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly AgentIDCardBoundUserInterface _bui;
|
||||
|
||||
private const int JobIconColumnCount = 10;
|
||||
|
||||
public event Action<string>? OnNameChanged;
|
||||
public event Action<string>? OnJobChanged;
|
||||
|
||||
public AgentIDCardWindow()
|
||||
public AgentIDCardWindow(AgentIDCardBoundUserInterface bui)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
|
||||
_bui = bui;
|
||||
|
||||
NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
|
||||
NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
|
||||
@@ -21,6 +38,51 @@ namespace Content.Client.Access.UI
|
||||
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
|
||||
}
|
||||
|
||||
public void SetAllowedIcons(HashSet<string> icons)
|
||||
{
|
||||
IconGrid.DisposeAllChildren();
|
||||
|
||||
var jobIconGroup = new ButtonGroup();
|
||||
var i = 0;
|
||||
foreach (var jobIconId in icons)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<StatusIconPrototype>(jobIconId, out var jobIcon))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
String styleBase = StyleBase.ButtonOpenBoth;
|
||||
var modulo = i % JobIconColumnCount;
|
||||
if (modulo == 0)
|
||||
styleBase = StyleBase.ButtonOpenRight;
|
||||
else if (modulo == JobIconColumnCount - 1)
|
||||
styleBase = StyleBase.ButtonOpenLeft;
|
||||
|
||||
// Generate buttons
|
||||
var jobIconButton = new Button
|
||||
{
|
||||
Access = AccessLevel.Public,
|
||||
StyleClasses = { styleBase },
|
||||
MaxSize = new Vector2(42, 28),
|
||||
Group = jobIconGroup,
|
||||
Pressed = i == 0,
|
||||
};
|
||||
|
||||
// Generate buttons textures
|
||||
TextureRect jobIconTexture = new TextureRect
|
||||
{
|
||||
Texture = _spriteSystem.Frame0(jobIcon.Icon),
|
||||
TextureScale = new Vector2(2.5f, 2.5f),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
};
|
||||
|
||||
jobIconButton.AddChild(jobIconTexture);
|
||||
jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID);
|
||||
IconGrid.AddChild(jobIconButton);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCurrentName(string name)
|
||||
{
|
||||
NameLineEdit.Text = name;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<LineEdit Name="PlayerNameLine" MinWidth="100" HorizontalExpand="True" PlaceHolder="{Loc ban-panel-player}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Margin="2">
|
||||
<CheckBox Name="IpCheckbox" MinWidth="100" Text="{Loc ban-panel-ip}" Pressed="False" />
|
||||
<CheckBox Name="IpCheckbox" MinWidth="100" Text="{Loc ban-panel-ip}" Pressed="True" />
|
||||
<Control MinWidth="50" />
|
||||
<LineEdit Name="IpLine" MinWidth="100" HorizontalExpand="True" PlaceHolder="{Loc ban-panel-ip}" ToolTip="{Loc ban-panel-ip-hwid-tooltip}" Editable="False" />
|
||||
</BoxContainer>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Chemistry;
|
||||
@@ -7,11 +5,12 @@ using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
|
||||
@@ -404,7 +404,7 @@ namespace Content.Client.Construction.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selected == null || _selected.Mirror == String.Empty)
|
||||
if (_selected == null || _selected.Mirror == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ using System.Numerics;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.CrewManifest;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
@@ -100,8 +102,15 @@ public sealed partial class CrewManifestUi : DefaultWindow
|
||||
|
||||
private sealed class CrewManifestSection : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
|
||||
private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
public CrewManifestSection(string sectionTitle, List<CrewManifestEntry> entries, IResourceCache cache, CrewManifestSystem crewManifestSystem)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
HorizontalExpand = true;
|
||||
|
||||
@@ -122,9 +131,6 @@ public sealed partial class CrewManifestUi : DefaultWindow
|
||||
|
||||
AddChild(gridContainer);
|
||||
|
||||
var path = new ResPath("/Textures/Interface/Misc/job_icons.rsi");
|
||||
cache.TryGetResource(path, out RSIResource? rsi);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var name = new RichTextLabel()
|
||||
@@ -143,25 +149,15 @@ public sealed partial class CrewManifestUi : DefaultWindow
|
||||
title.SetMessage(entry.JobTitle);
|
||||
|
||||
|
||||
if (rsi != null)
|
||||
if (_prototypeManager.TryIndex<StatusIconPrototype>(entry.JobIcon, out var jobIcon))
|
||||
{
|
||||
var icon = new TextureRect()
|
||||
{
|
||||
TextureScale = new Vector2(2, 2),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
Texture = _spriteSystem.Frame0(jobIcon.Icon),
|
||||
};
|
||||
|
||||
if (rsi.RSI.TryGetState(entry.JobIcon, out _))
|
||||
{
|
||||
var specifier = new SpriteSpecifier.Rsi(path, entry.JobIcon);
|
||||
icon.Texture = specifier.Frame0();
|
||||
}
|
||||
else if (rsi.RSI.TryGetState("Unknown", out _))
|
||||
{
|
||||
var specifier = new SpriteSpecifier.Rsi(path, "Unknown");
|
||||
icon.Texture = specifier.Frame0();
|
||||
}
|
||||
|
||||
titleContainer.AddChild(icon);
|
||||
titleContainer.AddChild(title);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ public sealed class DecalPlacementSystem : EntitySystem
|
||||
|
||||
public sealed class PlaceDecalActionEvent : WorldTargetActionEvent
|
||||
{
|
||||
[DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>))]
|
||||
[DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>), required:true)]
|
||||
public string DecalId = string.Empty;
|
||||
|
||||
[DataField("color")]
|
||||
|
||||
107
Content.Client/Effects/ColorFlashEffectSystem.cs
Normal file
107
Content.Client/Effects/ColorFlashEffectSystem.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using Content.Shared.Effects;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
|
||||
namespace Content.Client.Effects;
|
||||
|
||||
public sealed class ColorFlashEffectSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
|
||||
/// <summary>
|
||||
/// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register.
|
||||
/// </summary>
|
||||
private const float AnimationLength = 0.30f;
|
||||
private const string AnimationKey = "color-flash-effect";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeAllEvent<ColorFlashEffectEvent>(OnColorFlashEffect);
|
||||
SubscribeLocalEvent<ColorFlashEffectComponent, AnimationCompletedEvent>(OnEffectAnimationCompleted);
|
||||
}
|
||||
|
||||
private void OnEffectAnimationCompleted(EntityUid uid, ColorFlashEffectComponent component, AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != AnimationKey)
|
||||
return;
|
||||
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||
{
|
||||
sprite.Color = component.Color;
|
||||
}
|
||||
|
||||
RemCompDeferred<ColorFlashEffectComponent>(uid);
|
||||
}
|
||||
|
||||
private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(uid, ref sprite, false))
|
||||
return null;
|
||||
|
||||
// 90% of them are going to be this so why allocate a new class.
|
||||
return new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(AnimationLength),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Color),
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(color, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(sprite.Color, AnimationLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnColorFlashEffect(ColorFlashEffectEvent ev)
|
||||
{
|
||||
var color = ev.Color;
|
||||
|
||||
foreach (var ent in ev.Entities)
|
||||
{
|
||||
if (Deleted(ent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var player = EnsureComp<AnimationPlayerComponent>(ent);
|
||||
player.NetSyncEnabled = false;
|
||||
|
||||
// Need to stop the existing animation first to ensure the sprite color is fixed.
|
||||
// Otherwise we might lerp to a red colour instead.
|
||||
if (_animation.HasRunningAnimation(ent, player, AnimationKey))
|
||||
{
|
||||
_animation.Stop(ent, player, AnimationKey);
|
||||
}
|
||||
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp<ColorFlashEffectComponent>(ent, out var effect))
|
||||
{
|
||||
sprite.Color = effect.Color;
|
||||
}
|
||||
|
||||
var animation = GetDamageAnimation(ent, color, sprite);
|
||||
|
||||
if (animation == null)
|
||||
continue;
|
||||
|
||||
var comp = EnsureComp<ColorFlashEffectComponent>(ent);
|
||||
comp.NetSyncEnabled = false;
|
||||
comp.Color = sprite.Color;
|
||||
_animation.Play(player, animation, AnimationKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Content.Client.Flash
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
private readonly ShaderInstance _shader;
|
||||
private double _startTime = -1;
|
||||
private double _lastsFor = 1;
|
||||
@@ -61,18 +61,16 @@ namespace Content.Client.Flash
|
||||
if (percentComplete >= 1.0f)
|
||||
return;
|
||||
|
||||
var screenSpaceHandle = args.ScreenHandle;
|
||||
screenSpaceHandle.UseShader(_shader);
|
||||
var worldHandle = args.WorldHandle;
|
||||
worldHandle.UseShader(_shader);
|
||||
_shader.SetParameter("percentComplete", percentComplete);
|
||||
|
||||
var screenSize = UIBox2.FromDimensions(new Vector2(0, 0), _displayManager.ScreenSize);
|
||||
|
||||
if (_screenshotTexture != null)
|
||||
{
|
||||
screenSpaceHandle.DrawTextureRect(_screenshotTexture, screenSize);
|
||||
worldHandle.DrawTextureRectRegion(_screenshotTexture, args.WorldBounds);
|
||||
}
|
||||
|
||||
screenSpaceHandle.UseShader(null);
|
||||
worldHandle.UseShader(null);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
|
||||
45
Content.Client/Gateway/UI/GatewayBoundUserInterface.cs
Normal file
45
Content.Client/Gateway/UI/GatewayBoundUserInterface.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Content.Shared.Gateway;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Gateway.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GatewayBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private GatewayWindow? _window;
|
||||
|
||||
public GatewayBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new GatewayWindow();
|
||||
_window.OpenPortal += destination =>
|
||||
{
|
||||
SendMessage(new GatewayOpenPortalMessage(destination));
|
||||
};
|
||||
_window.OnClose += Close;
|
||||
_window?.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not GatewayBoundUserInterfaceState current)
|
||||
return;
|
||||
|
||||
_window?.UpdateState(current);
|
||||
}
|
||||
}
|
||||
22
Content.Client/Gateway/UI/GatewayWindow.xaml
Normal file
22
Content.Client/Gateway/UI/GatewayWindow.xaml
Normal file
@@ -0,0 +1,22 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'gateway-window-title'}"
|
||||
MinSize="800 360">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="NextCloseLabel"
|
||||
Text="{Loc 'gateway-window-portal-closing'}"
|
||||
Margin="5"></Label>
|
||||
<ProgressBar Name="NextCloseBar"
|
||||
HorizontalExpand="True"
|
||||
MinValue="0"
|
||||
MaxValue="1"
|
||||
SetHeight="25"/>
|
||||
<Label Name="NextCloseText" Text="0" Margin="5"/>
|
||||
</BoxContainer>
|
||||
<controls:HLine Color="#404040" Thickness="2" Margin="0 5 0 5"/>
|
||||
<BoxContainer Name="Container"
|
||||
Orientation="Vertical"
|
||||
Margin="5 0 5 0"/>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
180
Content.Client/Gateway/UI/GatewayWindow.xaml.cs
Normal file
180
Content.Client/Gateway/UI/GatewayWindow.xaml.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using Content.Client.Computer;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Gateway;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Gateway.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GatewayWindow : FancyWindow,
|
||||
IComputerWindow<EmergencyConsoleBoundUserInterfaceState>
|
||||
{
|
||||
private readonly IGameTiming _timing;
|
||||
|
||||
public event Action<EntityUid>? OpenPortal;
|
||||
private List<(EntityUid, string, TimeSpan, bool)> _destinations = default!;
|
||||
private EntityUid? _current;
|
||||
private TimeSpan _nextClose;
|
||||
private TimeSpan _lastOpen;
|
||||
private List<Label> _readyLabels = default!;
|
||||
private List<Button> _openButtons = default!;
|
||||
|
||||
public GatewayWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_timing = IoCManager.Resolve<IGameTiming>();
|
||||
}
|
||||
|
||||
public void UpdateState(GatewayBoundUserInterfaceState state)
|
||||
{
|
||||
_destinations = state.Destinations;
|
||||
_current = state.Current;
|
||||
_nextClose = state.NextClose;
|
||||
_lastOpen = state.LastOpen;
|
||||
|
||||
Container.DisposeAllChildren();
|
||||
_readyLabels = new List<Label>(_destinations.Count);
|
||||
_openButtons = new List<Button>(_destinations.Count);
|
||||
|
||||
if (_destinations.Count == 0)
|
||||
{
|
||||
Container.AddChild(new BoxContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("gateway-window-no-destinations"),
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var now = _timing.CurTime;
|
||||
foreach (var dest in _destinations)
|
||||
{
|
||||
var uid = dest.Item1;
|
||||
var name = dest.Item2;
|
||||
var nextReady = dest.Item3;
|
||||
var busy = dest.Item4;
|
||||
|
||||
var box = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Margin = new Thickness(5f, 5f)
|
||||
};
|
||||
|
||||
box.AddChild(new Label()
|
||||
{
|
||||
Text = name
|
||||
});
|
||||
|
||||
var readyLabel = new Label
|
||||
{
|
||||
Text = ReadyText(now, nextReady),
|
||||
Margin = new Thickness(10f, 0f, 0f, 0f)
|
||||
};
|
||||
_readyLabels.Add(readyLabel);
|
||||
box.AddChild(readyLabel);
|
||||
|
||||
var openButton = new Button()
|
||||
{
|
||||
Text = Loc.GetString("gateway-window-open-portal"),
|
||||
Pressed = uid == _current,
|
||||
ToggleMode = true,
|
||||
Disabled = _current != null || busy || now < nextReady
|
||||
};
|
||||
|
||||
openButton.OnPressed += args =>
|
||||
{
|
||||
OpenPortal?.Invoke(uid);
|
||||
};
|
||||
|
||||
if (uid == state.Current)
|
||||
{
|
||||
openButton.AddStyleClass(StyleBase.ButtonCaution);
|
||||
}
|
||||
|
||||
_openButtons.Add(openButton);
|
||||
box.AddChild(new BoxContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Align = BoxContainer.AlignMode.End,
|
||||
Children =
|
||||
{
|
||||
openButton
|
||||
}
|
||||
});
|
||||
|
||||
Container.AddChild(new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat(new Color(30, 30, 34)),
|
||||
Margin = new Thickness(10f, 5f),
|
||||
Children =
|
||||
{
|
||||
box
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var now = _timing.CurTime;
|
||||
|
||||
// if its not going to close then show it as empty
|
||||
if (_current == null)
|
||||
{
|
||||
NextCloseBar.Value = 0f;
|
||||
NextCloseText.Text = "00:00";
|
||||
}
|
||||
else
|
||||
{
|
||||
var remaining = _nextClose - _timing.CurTime;
|
||||
if (remaining < TimeSpan.Zero)
|
||||
{
|
||||
NextCloseBar.Value = 1f;
|
||||
NextCloseText.Text = "00:00";
|
||||
}
|
||||
else
|
||||
{
|
||||
var openTime = _nextClose - _lastOpen;
|
||||
NextCloseBar.Value = 1f - (float) (remaining / openTime);
|
||||
NextCloseText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < _destinations.Count; i++)
|
||||
{
|
||||
var dest = _destinations[i];
|
||||
var nextReady = dest.Item3;
|
||||
var busy = dest.Item4;
|
||||
_readyLabels[i].Text = ReadyText(now, nextReady);
|
||||
_openButtons[i].Disabled = _current != null || busy || now < nextReady;
|
||||
}
|
||||
}
|
||||
|
||||
private string ReadyText(TimeSpan now, TimeSpan nextReady)
|
||||
{
|
||||
if (now < nextReady)
|
||||
{
|
||||
var time = nextReady - now;
|
||||
return Loc.GetString("gateway-window-ready-in", ("time", $"{time.Minutes:00}:{time.Seconds:00}"));
|
||||
}
|
||||
|
||||
return Loc.GetString("gateway-window-ready");
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Players.PlayTimeTracking;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -233,8 +234,8 @@ namespace Content.Client.LateJoin
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
|
||||
var specifier = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/job_icons.rsi"), prototype.Icon);
|
||||
icon.Texture = _sprites.Frame0(specifier);
|
||||
var jobIcon = _prototypeManager.Index<StatusIconPrototype>(prototype.Icon);
|
||||
icon.Texture = _sprites.Frame0(jobIcon.Icon);
|
||||
jobSelector.AddChild(icon);
|
||||
|
||||
var jobLabel = new Label
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.LateJoin;
|
||||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Preferences;
|
||||
using Content.Client.Preferences.UI;
|
||||
using Content.Client.UserInterface.Systems.Chat;
|
||||
using Content.Client.Voting;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
@@ -199,6 +196,29 @@ namespace Content.Client.Lobby
|
||||
{
|
||||
_lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
|
||||
}
|
||||
|
||||
if (_gameTicker.LobbySong == null)
|
||||
{
|
||||
_lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
|
||||
}
|
||||
else if (_resourceCache.TryGetResource<AudioResource>(_gameTicker.LobbySong, out var lobbySongResource))
|
||||
{
|
||||
var lobbyStream = lobbySongResource.AudioStream;
|
||||
|
||||
var title = string.IsNullOrEmpty(lobbyStream.Title) ?
|
||||
Loc.GetString("lobby-state-song-unknown-title") :
|
||||
lobbyStream.Title;
|
||||
|
||||
var artist = string.IsNullOrEmpty(lobbyStream.Artist) ?
|
||||
Loc.GetString("lobby-state-song-unknown-artist") :
|
||||
lobbyStream.Artist;
|
||||
|
||||
var markup = Loc.GetString("lobby-state-song-text",
|
||||
("songTitle", title),
|
||||
("songArtist", artist));
|
||||
|
||||
_lobby!.LobbySong.SetMarkup(markup);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLobbyBackground()
|
||||
|
||||
@@ -40,8 +40,11 @@
|
||||
<!-- Vertical Padding-->
|
||||
<Control VerticalExpand="True"/>
|
||||
<!-- Left Bot Panel -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Bottom" MaxWidth="620">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Bottom">
|
||||
<info:DevInfoBanner Name="DevInfoBanner" VerticalExpand="false" Margin="3 3 3 3"/>
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<RichTextLabel Name="LobbySong" Access="Public" HorizontalAlignment="Center" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
<!-- Character setup state -->
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<PanelContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#202124" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<RichTextLabel Name="Name" Margin="8 8 8 8" HorizontalAlignment="Left"/>
|
||||
<Button Name="Delete"
|
||||
MinWidth="30"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{Loc 'news-write-ui-delete-text'}"
|
||||
Access="Public"
|
||||
Margin="6 6 6 6">
|
||||
</Button>
|
||||
</PanelContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 12">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#4c6530"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<Label Name="Name" Margin="6 6 6 6" HorizontalAlignment="Center"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#33333f"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<RichTextLabel Name="Author" HorizontalExpand="True" VerticalAlignment="Bottom" Margin="6 6 6 6"/>
|
||||
<Button Name="Delete"
|
||||
Text="{Loc 'news-write-ui-delete-text'}"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="8 6 6 6"
|
||||
Access="Public"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -15,11 +15,12 @@ public sealed partial class MiniArticleCardControl : Control
|
||||
public Action? OnDeletePressed;
|
||||
public int ArtcileNum;
|
||||
|
||||
public MiniArticleCardControl(string name)
|
||||
public MiniArticleCardControl(string name, string author)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Name.SetMarkup(name);
|
||||
Name.Text = name;
|
||||
Author.SetMarkup(author);
|
||||
|
||||
Delete.OnPressed += _ => OnDeletePressed?.Invoke();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ public sealed partial class NewsWriteMenu : DefaultWindow
|
||||
|
||||
for (int i = 0; i < articles.Length; i++)
|
||||
{
|
||||
var mini = new MiniArticleCardControl(articles[i].Name);
|
||||
var article = articles[i];
|
||||
var mini = new MiniArticleCardControl(article.Name, (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
|
||||
mini.ArtcileNum = i;
|
||||
mini.OnDeletePressed += () => DeleteButtonPressed?.Invoke(mini.ArtcileNum);
|
||||
|
||||
|
||||
117
Content.Client/Overlays/EquipmentHudSystem.cs
Normal file
117
Content.Client/Overlays/EquipmentHudSystem.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// This is a base system to make it easier to enable or disabling UI elements based on whether or not the player has
|
||||
/// some component, either on their controlled entity on some worn piece of equipment.
|
||||
/// </summary>
|
||||
public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
protected bool IsActive;
|
||||
protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<T, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<T, ComponentRemove>(OnRemove);
|
||||
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
SubscribeLocalEvent<T, GotEquippedEvent>(OnCompEquip);
|
||||
SubscribeLocalEvent<T, GotUnequippedEvent>(OnCompUnequip);
|
||||
|
||||
SubscribeLocalEvent<T, RefreshEquipmentHudEvent<T>>(OnRefreshComponentHud);
|
||||
SubscribeLocalEvent<T, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>>>(OnRefreshEquipmentHud);
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
}
|
||||
|
||||
private void Update(RefreshEquipmentHudEvent<T> ev)
|
||||
{
|
||||
IsActive = true;
|
||||
UpdateInternal(ev);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
IsActive = false;
|
||||
DeactivateInternal();
|
||||
}
|
||||
|
||||
protected virtual void UpdateInternal(RefreshEquipmentHudEvent<T> args) { }
|
||||
|
||||
protected virtual void DeactivateInternal() { }
|
||||
|
||||
private void OnStartup(EntityUid uid, T component, ComponentStartup args)
|
||||
{
|
||||
RefreshOverlay(uid);
|
||||
}
|
||||
|
||||
private void OnRemove(EntityUid uid, T component, ComponentRemove args)
|
||||
{
|
||||
RefreshOverlay(uid);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Entity);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(PlayerDetachedEvent args)
|
||||
{
|
||||
if (_player.LocalPlayer?.ControlledEntity == null)
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Equipee);
|
||||
}
|
||||
|
||||
private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Equipee);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent args)
|
||||
{
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
protected virtual void OnRefreshEquipmentHud(EntityUid uid, T component, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
|
||||
{
|
||||
args.Args.Active = true;
|
||||
}
|
||||
|
||||
protected virtual void OnRefreshComponentHud(EntityUid uid, T component, RefreshEquipmentHudEvent<T> args)
|
||||
{
|
||||
args.Active = true;
|
||||
}
|
||||
|
||||
private void RefreshOverlay(EntityUid uid)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
var ev = new RefreshEquipmentHudEvent<T>(TargetSlots);
|
||||
RaiseLocalEvent(uid, ev);
|
||||
|
||||
if (ev.Active)
|
||||
Update(ev);
|
||||
else
|
||||
Deactivate();
|
||||
}
|
||||
}
|
||||
73
Content.Client/Overlays/ShowSecurityIconsSystem.cs
Normal file
73
Content.Client/Overlays/ShowSecurityIconsSystem.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Overlays;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
public sealed class ShowSecurityIconsSystem : EquipmentHudSystem<ShowSecurityIconsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeMan = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
|
||||
[ValidatePrototypeId<StatusIconPrototype>]
|
||||
private const string JobIconForNoId = "JobIconNoId";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StatusIconComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
}
|
||||
|
||||
private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent @event)
|
||||
{
|
||||
if (!IsActive || @event.InContainer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var healthIcons = DecideSecurityIcon(uid);
|
||||
|
||||
@event.StatusIcons.AddRange(healthIcons);
|
||||
}
|
||||
|
||||
private IReadOnlyList<StatusIconPrototype> DecideSecurityIcon(EntityUid uid)
|
||||
{
|
||||
var result = new List<StatusIconPrototype>();
|
||||
|
||||
var jobIconToGet = JobIconForNoId;
|
||||
if (_accessReader.FindAccessItemsInventory(uid, out var items))
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
// ID Card
|
||||
if (TryComp(item, out IdCardComponent? id))
|
||||
{
|
||||
jobIconToGet = id.JobIcon;
|
||||
break;
|
||||
}
|
||||
|
||||
// PDA
|
||||
if (TryComp(item, out PdaComponent? pda)
|
||||
&& pda.ContainedId != null
|
||||
&& TryComp(pda.ContainedId, out id))
|
||||
{
|
||||
jobIconToGet = id.JobIcon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_prototypeMan.TryIndex<StatusIconPrototype>(jobIconToGet, out var jobIcon))
|
||||
result.Add(jobIcon);
|
||||
else
|
||||
Log.Error($"Invalid job icon prototype: {jobIcon}");
|
||||
|
||||
// Add arrest icons here, WYCI.
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ namespace Content.Client.Physics.Controllers
|
||||
{
|
||||
Physics.UpdateIsPredicted(uid);
|
||||
Physics.UpdateIsPredicted(component.RelayEntity);
|
||||
if (TryComp<InputMoverComponent>(component.RelayEntity, out var inputMover))
|
||||
if (MoverQuery.TryGetComponent(component.RelayEntity, out var inputMover))
|
||||
SetMoveInput(inputMover, MoveButtons.None);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Content.Client.Physics.Controllers
|
||||
{
|
||||
Physics.UpdateIsPredicted(uid);
|
||||
Physics.UpdateIsPredicted(component.RelayEntity);
|
||||
if (TryComp<InputMoverComponent>(component.RelayEntity, out var inputMover))
|
||||
if (MoverQuery.TryGetComponent(component.RelayEntity, out var inputMover))
|
||||
SetMoveInput(inputMover, MoveButtons.None);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace Content.Client.Physics.Controllers
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} player)
|
||||
return;
|
||||
|
||||
if (TryComp<RelayInputMoverComponent>(player, out var relayMover))
|
||||
if (RelayQuery.TryGetComponent(player, out var relayMover))
|
||||
HandleClientsideMovement(relayMover.RelayEntity, frameTime);
|
||||
|
||||
HandleClientsideMovement(player, frameTime);
|
||||
@@ -95,15 +95,8 @@ namespace Content.Client.Physics.Controllers
|
||||
|
||||
private void HandleClientsideMovement(EntityUid player, float frameTime)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var moverQuery = GetEntityQuery<InputMoverComponent>();
|
||||
var relayTargetQuery = GetEntityQuery<MovementRelayTargetComponent>();
|
||||
var mobMoverQuery = GetEntityQuery<MobMoverComponent>();
|
||||
var pullableQuery = GetEntityQuery<SharedPullableComponent>();
|
||||
var modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
|
||||
|
||||
if (!moverQuery.TryGetComponent(player, out var mover) ||
|
||||
!xformQuery.TryGetComponent(player, out var xform))
|
||||
if (!MoverQuery.TryGetComponent(player, out var mover) ||
|
||||
!XformQuery.TryGetComponent(player, out var xform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -112,17 +105,17 @@ namespace Content.Client.Physics.Controllers
|
||||
PhysicsComponent? body;
|
||||
var xformMover = xform;
|
||||
|
||||
if (mover.ToParent && HasComp<RelayInputMoverComponent>(xform.ParentUid))
|
||||
if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
|
||||
{
|
||||
if (!TryComp(xform.ParentUid, out body) ||
|
||||
!TryComp(xform.ParentUid, out xformMover))
|
||||
if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
|
||||
!XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
physicsUid = xform.ParentUid;
|
||||
}
|
||||
else if (!TryComp(player, out body))
|
||||
else if (!PhysicsQuery.TryGetComponent(player, out body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -134,13 +127,7 @@ namespace Content.Client.Physics.Controllers
|
||||
physicsUid,
|
||||
body,
|
||||
xformMover,
|
||||
frameTime,
|
||||
xformQuery,
|
||||
moverQuery,
|
||||
mobMoverQuery,
|
||||
relayTargetQuery,
|
||||
pullableQuery,
|
||||
modifierQuery);
|
||||
frameTime);
|
||||
}
|
||||
|
||||
protected override bool CanSound()
|
||||
|
||||
26
Content.Client/Power/Generator/GeneratorWindow.xaml
Normal file
26
Content.Client/Power/Generator/GeneratorWindow.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="350 130"
|
||||
SetSize="360 180"
|
||||
Title="{Loc 'generator-ui-title'}">
|
||||
<BoxContainer Margin="4 0" Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2" VerticalAlignment="Center" Margin="5">
|
||||
<GridContainer Margin="2 0 0 0" Columns="2" HorizontalExpand="True">
|
||||
<!-- Power -->
|
||||
<Label Text="{Loc 'generator-ui-target-power-label'}"/>
|
||||
<SpinBox Name="TargetPower" HorizontalExpand="True"/>
|
||||
<Label Text="{Loc 'generator-ui-efficiency-label'}"/>
|
||||
<Label Name="Efficiency" Text="???%" HorizontalExpand="True"/>
|
||||
<Label Text="{Loc 'generator-ui-fuel-use-label'}"/>
|
||||
<ProgressBar Name="FuelFraction" MinValue="0" MaxValue="1" HorizontalExpand="True"/>
|
||||
<Label Text="{Loc 'generator-ui-fuel-left-label'}"/>
|
||||
<Label Name="FuelLeft" Text="0" HorizontalExpand="True"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
<cc:VSeparator StyleClasses="LowDivider"/>
|
||||
<PanelContainer Margin="12 0 0 0" StyleClasses="Inset" VerticalAlignment="Center">
|
||||
<SpriteView Name="EntityView" SetSize="64 64" Scale="2 2" OverrideDirection="South" Margin="15"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
57
Content.Client/Power/Generator/GeneratorWindow.xaml.cs
Normal file
57
Content.Client/Power/Generator/GeneratorWindow.xaml.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Power.Generator;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Power.Generator;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GeneratorWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly FuelGeneratorComponent? _component;
|
||||
private SolidFuelGeneratorComponentBuiState? _lastState;
|
||||
|
||||
public GeneratorWindow(SolidFuelGeneratorBoundUserInterface bui, EntityUid vis)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_entityManager.TryGetComponent(vis, out _component);
|
||||
|
||||
EntityView.SetEntity(vis);
|
||||
TargetPower.IsValid += IsValid;
|
||||
TargetPower.ValueChanged += (args) =>
|
||||
{
|
||||
bui.SetTargetPower(args.Value);
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsValid(int arg)
|
||||
{
|
||||
if (arg < 0)
|
||||
return false;
|
||||
|
||||
if (arg > (_lastState?.MaximumPower / 1000.0f ?? 0))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Update(SolidFuelGeneratorComponentBuiState state)
|
||||
{
|
||||
if (_component == null)
|
||||
return;
|
||||
|
||||
var oldState = _lastState;
|
||||
_lastState = state;
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
if (oldState?.TargetPower != state.TargetPower)
|
||||
TargetPower.OverrideValue((int)(state.TargetPower / 1000.0f));
|
||||
Efficiency.Text = SharedGeneratorSystem.CalcFuelEfficiency(state.TargetPower, state.OptimalPower, _component).ToString("P1");
|
||||
FuelFraction.Value = state.RemainingFuel - (int) state.RemainingFuel;
|
||||
FuelLeft.Text = ((int) MathF.Floor(state.RemainingFuel)).ToString();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.Power.Generator;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Power.Generator;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SolidFuelGeneratorBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private GeneratorWindow? _window;
|
||||
|
||||
public SolidFuelGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = new GeneratorWindow(this, Owner);
|
||||
|
||||
_window.OpenCenteredLeft();
|
||||
_window.OnClose += Close;
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is not SolidFuelGeneratorComponentBuiState msg)
|
||||
return;
|
||||
|
||||
_window?.Update(msg);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_window?.Dispose();
|
||||
}
|
||||
|
||||
public void SetTargetPower(int target)
|
||||
{
|
||||
SendMessage(new SetTargetPowerMessage(target));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.Traits;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -584,7 +585,7 @@ namespace Content.Client.Preferences.UI
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
var selector = new JobPrioritySelector(job);
|
||||
var selector = new JobPrioritySelector(job, _prototypeManager);
|
||||
|
||||
if (!_requirements.IsAllowed(job, out var reason))
|
||||
{
|
||||
@@ -1212,7 +1213,7 @@ namespace Content.Client.Preferences.UI
|
||||
private Label _requirementsLabel;
|
||||
private Label _jobTitle;
|
||||
|
||||
public JobPrioritySelector(JobPrototype job)
|
||||
public JobPrioritySelector(JobPrototype job, IPrototypeManager prototypeManager)
|
||||
{
|
||||
Job = job;
|
||||
|
||||
@@ -1242,12 +1243,8 @@ namespace Content.Client.Preferences.UI
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
|
||||
if (job.Icon != null)
|
||||
{
|
||||
var specifier = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/job_icons.rsi"),
|
||||
job.Icon);
|
||||
icon.Texture = specifier.Frame0();
|
||||
}
|
||||
var jobIcon = prototypeManager.Index<StatusIconPrototype>(job.Icon);
|
||||
icon.Texture = jobIcon.Icon.Frame0();
|
||||
|
||||
_requirementsLabel = new Label()
|
||||
{
|
||||
|
||||
@@ -5,13 +5,13 @@ using Content.Client.Replay.Spectator;
|
||||
using Content.Client.Replay.UI.Loading;
|
||||
using Content.Client.UserInterface.Systems.Chat;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Effects;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.GameWindow;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
@@ -24,9 +24,7 @@ using Robust.Client.Replays.Playback;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Replay;
|
||||
|
||||
@@ -66,7 +64,7 @@ public sealed class ContentReplayPlaybackManager
|
||||
private void LoadOverride(IReplayFileReader fileReader)
|
||||
{
|
||||
var screen = _stateMan.RequestStateChange<LoadingScreen<bool>>();
|
||||
screen.Job = new ContentLoadReplayJob(1/60f, fileReader, _loadMan, screen);
|
||||
screen.Job = new ContentLoadReplayJob(1 / 60f, fileReader, _loadMan, screen);
|
||||
screen.OnJobFinished += (_, e) => OnFinishedLoading(e);
|
||||
}
|
||||
|
||||
@@ -141,7 +139,7 @@ public sealed class ContentReplayPlaybackManager
|
||||
case SharedGunSystem.HitscanEvent:
|
||||
case ImpactEffectEvent:
|
||||
case MuzzleFlashEvent:
|
||||
case DamageEffectEvent:
|
||||
case ColorFlashEffectEvent:
|
||||
case InstrumentStartMidiEvent:
|
||||
case InstrumentMidiEventEvent:
|
||||
case InstrumentStopMidiEvent:
|
||||
@@ -159,7 +157,7 @@ public sealed class ContentReplayPlaybackManager
|
||||
|
||||
private void OnReplayPlaybackStopped()
|
||||
{
|
||||
_conGrp.Implementation = (IClientConGroupImplementation)_adminMan;
|
||||
_conGrp.Implementation = (IClientConGroupImplementation) _adminMan;
|
||||
ReturnToDefaultState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (Direction == DirectionFlag.None)
|
||||
{
|
||||
if (TryComp(player, out InputMoverComponent? cmp))
|
||||
_mover.LerpRotation(cmp, frameTime);
|
||||
_mover.LerpRotation(player, cmp, frameTime);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (!TryComp(player, out InputMoverComponent? mover))
|
||||
return;
|
||||
|
||||
_mover.LerpRotation(mover, frameTime);
|
||||
_mover.LerpRotation(player, mover, frameTime);
|
||||
|
||||
var effectiveDir = Direction;
|
||||
if ((Direction & DirectionFlag.North) != 0)
|
||||
@@ -75,7 +76,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
|
||||
var query = GetEntityQuery<TransformComponent>();
|
||||
var xform = query.GetComponent(player);
|
||||
var pos = _transform.GetWorldPosition(xform, query);
|
||||
var pos = _transform.GetWorldPosition(xform);
|
||||
|
||||
if (!xform.ParentUid.IsValid())
|
||||
{
|
||||
@@ -93,12 +94,12 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (xform.ParentUid.IsValid())
|
||||
_transform.SetGridId(player, xform, Transform(xform.ParentUid).GridUid);
|
||||
|
||||
var parentRotation = _mover.GetParentGridAngle(mover, query);
|
||||
var parentRotation = _mover.GetParentGridAngle(mover);
|
||||
var localVec = effectiveDir.AsDir().ToAngle().ToWorldVec();
|
||||
var worldVec = parentRotation.RotateVec(localVec);
|
||||
var speed = CompOrNull<MovementSpeedModifierComponent>(player)?.BaseSprintSpeed ?? DefaultSpeed;
|
||||
var delta = worldVec * frameTime * speed;
|
||||
_transform.SetWorldPositionRotation(xform, pos + delta, delta.ToWorldAngle(), query);
|
||||
_transform.SetWorldPositionRotation(xform, pos + delta, delta.ToWorldAngle());
|
||||
}
|
||||
|
||||
private sealed class MoverHandler : InputCmdHandler
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Shared.Salvage;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Salvage;
|
||||
|
||||
[NetworkedComponent, RegisterComponent]
|
||||
public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.StatusIcon;
|
||||
|
||||
@@ -41,10 +42,6 @@ public sealed class StatusIconOverlay : Overlay
|
||||
if (xform.MapID != args.MapId)
|
||||
continue;
|
||||
|
||||
var icons = _statusIcon.GetStatusIcons(uid, meta);
|
||||
if (icons.Count == 0)
|
||||
continue;
|
||||
|
||||
var bounds = comp.Bounds ?? sprite.Bounds;
|
||||
|
||||
var worldPos = _transform.GetWorldPosition(xform, xformQuery);
|
||||
@@ -52,12 +49,17 @@ public sealed class StatusIconOverlay : Overlay
|
||||
if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
|
||||
continue;
|
||||
|
||||
var icons = _statusIcon.GetStatusIcons(uid, meta);
|
||||
if (icons.Count == 0)
|
||||
continue;
|
||||
|
||||
var worldMatrix = Matrix3.CreateTranslation(worldPos);
|
||||
Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
|
||||
Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
|
||||
handle.SetTransform(matty);
|
||||
|
||||
var count = 0;
|
||||
var countL = 0;
|
||||
var countR = 0;
|
||||
var accOffsetL = 0;
|
||||
var accOffsetR = 0;
|
||||
icons.Sort();
|
||||
@@ -71,13 +73,16 @@ public sealed class StatusIconOverlay : Overlay
|
||||
|
||||
// the icons are ordered left to right, top to bottom.
|
||||
// extra icons that don't fit are just cut off.
|
||||
if (count % 2 == 0)
|
||||
if (proto.LocationPreference == StatusIconLocationPreference.Left ||
|
||||
proto.LocationPreference == StatusIconLocationPreference.None && countL <= countR)
|
||||
{
|
||||
if (accOffsetL + texture.Height > sprite.Bounds.Height * EyeManager.PixelsPerMeter)
|
||||
break;
|
||||
accOffsetL += texture.Height;
|
||||
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetL / EyeManager.PixelsPerMeter;
|
||||
xOffset = -(bounds.Width + sprite.Offset.X) / 2f;
|
||||
|
||||
countL++;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -86,8 +91,9 @@ public sealed class StatusIconOverlay : Overlay
|
||||
accOffsetR += texture.Height;
|
||||
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetR / EyeManager.PixelsPerMeter;
|
||||
xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float) texture.Width / EyeManager.PixelsPerMeter;
|
||||
|
||||
countR++;
|
||||
}
|
||||
count++;
|
||||
|
||||
var position = new Vector2(xOffset, yOffset);
|
||||
handle.DrawTexture(texture, position);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Weapons.Melee.Components;
|
||||
using Content.Shared.Weapons;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
@@ -11,106 +9,10 @@ namespace Content.Client.Weapons.Melee;
|
||||
|
||||
public sealed partial class MeleeWeaponSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register.
|
||||
/// </summary>
|
||||
private const float DamageAnimationLength = 0.30f;
|
||||
|
||||
private const string DamageAnimationKey = "damage-effect";
|
||||
private const string FadeAnimationKey = "melee-fade";
|
||||
private const string SlashAnimationKey = "melee-slash";
|
||||
private const string ThrustAnimationKey = "melee-thrust";
|
||||
|
||||
private void InitializeEffect()
|
||||
{
|
||||
SubscribeLocalEvent<DamageEffectComponent, AnimationCompletedEvent>(OnEffectAnimation);
|
||||
}
|
||||
|
||||
private void OnEffectAnimation(EntityUid uid, DamageEffectComponent component, AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != DamageAnimationKey)
|
||||
return;
|
||||
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||
{
|
||||
sprite.Color = component.Color;
|
||||
}
|
||||
|
||||
RemCompDeferred<DamageEffectComponent>(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the red effect animation whenever the server confirms something is hit
|
||||
/// </summary>
|
||||
private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(uid, ref sprite, false))
|
||||
return null;
|
||||
|
||||
// 90% of them are going to be this so why allocate a new class.
|
||||
return new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(DamageAnimationLength),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Color),
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(color, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(sprite.Color, DamageAnimationLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnDamageEffect(DamageEffectEvent ev)
|
||||
{
|
||||
var color = ev.Color;
|
||||
|
||||
foreach (var ent in ev.Entities)
|
||||
{
|
||||
if (Deleted(ent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var player = EnsureComp<AnimationPlayerComponent>(ent);
|
||||
player.NetSyncEnabled = false;
|
||||
|
||||
// Need to stop the existing animation first to ensure the sprite color is fixed.
|
||||
// Otherwise we might lerp to a red colour instead.
|
||||
if (_animation.HasRunningAnimation(ent, player, DamageAnimationKey))
|
||||
{
|
||||
_animation.Stop(ent, player, DamageAnimationKey);
|
||||
}
|
||||
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp<DamageEffectComponent>(ent, out var effect))
|
||||
{
|
||||
sprite.Color = effect.Color;
|
||||
}
|
||||
|
||||
var animation = GetDamageAnimation(ent, color, sprite);
|
||||
|
||||
if (animation == null)
|
||||
continue;
|
||||
|
||||
var comp = EnsureComp<DamageEffectComponent>(ent);
|
||||
comp.NetSyncEnabled = false;
|
||||
comp.Color = sprite.Color;
|
||||
_animation.Play(player, animation, DamageAnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Effects;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
@@ -37,9 +38,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
InitializeEffect();
|
||||
_overlayManager.AddOverlay(new MeleeWindupOverlay(EntityManager, _timing, _player, _protoManager));
|
||||
SubscribeAllEvent<DamageEffectEvent>(OnDamageEffect);
|
||||
SubscribeNetworkEvent<MeleeLungeEvent>(OnMeleeLunge);
|
||||
UpdatesOutsidePrediction = true;
|
||||
}
|
||||
@@ -227,7 +226,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
{
|
||||
// Server never sends the event to us for predictiveeevent.
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
RaiseLocalEvent(new DamageEffectEvent(Color.Red, targets));
|
||||
RaiseLocalEvent(new ColorFlashEffectEvent(Color.Red, targets));
|
||||
}
|
||||
|
||||
protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Numerics;
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Weapons.Ranged.Components;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Spawners.Components;
|
||||
using Content.Shared.Weapons.Ranged;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
|
||||
@@ -9,11 +9,12 @@ using Content.IntegrationTests.Tests;
|
||||
using Content.IntegrationTests.Tests.Destructible;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.IntegrationTests.Tests.Interaction.Click;
|
||||
using Content.IntegrationTests.Tests.Networking;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Client;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -37,13 +38,15 @@ namespace Content.IntegrationTests;
|
||||
/// </summary>
|
||||
public static class PoolManager
|
||||
{
|
||||
public const string TestMap = "Empty";
|
||||
|
||||
private static readonly (string cvar, string value)[] ServerTestCvars =
|
||||
{
|
||||
// @formatter:off
|
||||
(CCVars.DatabaseSynchronous.Name, "true"),
|
||||
(CCVars.DatabaseSqliteDelay.Name, "0"),
|
||||
(CCVars.HolidaysEnabled.Name, "false"),
|
||||
(CCVars.GameMap.Name, "Empty"),
|
||||
(CCVars.GameMap.Name, TestMap),
|
||||
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
|
||||
(CVars.NetPVS.Name, "false"),
|
||||
(CCVars.NPCMaxUpdates.Name, "999999"),
|
||||
@@ -106,15 +109,6 @@ public static class PoolManager
|
||||
{
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
var compFactory = IoCManager.Resolve<IComponentFactory>();
|
||||
entSysMan.LoadExtraSystemType<AutoPredictReconcileTest.AutoPredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<AutoPredictionTestComponent>();
|
||||
entSysMan.LoadExtraSystemType<SimplePredictReconcileTest.PredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<SimplePredictReconcileTest.PredictionTestComponent>();
|
||||
entSysMan.LoadExtraSystemType<SystemPredictReconcileTest.SystemPredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<SystemPredictReconcileTest.SystemPredictionTestComponent>();
|
||||
IoCManager.Register<ResettingEntitySystemTests.TestRoundRestartCleanupEvent>();
|
||||
IoCManager.Register<InteractionSystemTests.TestInteractionSystem>();
|
||||
IoCManager.Register<DeviceNetworkTestSystem>();
|
||||
entSysMan.LoadExtraSystemType<ResettingEntitySystemTests.TestRoundRestartCleanupEvent>();
|
||||
entSysMan.LoadExtraSystemType<InteractionSystemTests.TestInteractionSystem>();
|
||||
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
|
||||
@@ -210,14 +204,8 @@ public static class PoolManager
|
||||
{
|
||||
ClientBeforeIoC = () =>
|
||||
{
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
var compFactory = IoCManager.Resolve<IComponentFactory>();
|
||||
entSysMan.LoadExtraSystemType<AutoPredictReconcileTest.AutoPredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<AutoPredictionTestComponent>();
|
||||
entSysMan.LoadExtraSystemType<SimplePredictReconcileTest.PredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<SimplePredictReconcileTest.PredictionTestComponent>();
|
||||
entSysMan.LoadExtraSystemType<SystemPredictReconcileTest.SystemPredictionTestEntitySystem>();
|
||||
compFactory.RegisterClass<SystemPredictReconcileTest.SystemPredictionTestComponent>();
|
||||
// do not register extra systems or components here -- they will get cleared when the client is
|
||||
// disconnected. just use reflection.
|
||||
IoCManager.Register<IParallaxManager, DummyParallaxManager>(true);
|
||||
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
IoCManager.Resolve<IConfigurationManager>()
|
||||
@@ -310,8 +298,12 @@ public static class PoolManager
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
|
||||
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
|
||||
|
||||
var cCfg = pair.Client.ResolveDependency<IConfigurationManager>();
|
||||
cCfg.SetCVar(CCVars.NetInterp, !poolSettings.DisableInterpolate);
|
||||
|
||||
if (canSkip)
|
||||
{
|
||||
ValidateFastRecycle(pair);
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
|
||||
}
|
||||
else
|
||||
@@ -319,6 +311,11 @@ public static class PoolManager
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
|
||||
await CleanPooledPair(poolSettings, pair, testOut);
|
||||
}
|
||||
|
||||
// Ensure client is 1 tick ahead of server? I don't think theres a real reason for why it should be
|
||||
// 1 tick specifically, I am just ensuring consistency with CreateServerClientPair()
|
||||
if (!pair.Settings.NotConnected)
|
||||
await SyncTicks(pair, targetDelta: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -357,6 +354,36 @@ public static class PoolManager
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidateFastRecycle(Pair pair)
|
||||
{
|
||||
if (pair.Settings.NoClient || pair.Settings.NoServer)
|
||||
return;
|
||||
|
||||
var baseClient = pair.Client.ResolveDependency<IBaseClient>();
|
||||
var netMan = pair.Client.ResolveDependency<INetManager>();
|
||||
Assert.That(netMan.IsConnected, Is.Not.EqualTo(pair.Settings.NotConnected));
|
||||
|
||||
if (pair.Settings.NotConnected)
|
||||
return;
|
||||
|
||||
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
|
||||
|
||||
var cPlayer = pair.Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var sPlayer = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
|
||||
Assert.That(cPlayer.LocalPlayer?.Session?.UserId, Is.EqualTo(sPlayer.Sessions.Single().UserId));
|
||||
|
||||
var ticker = pair.Server.ResolveDependency<EntityManager>().System<GameTicker>();
|
||||
Assert.That(ticker.DummyTicker, Is.EqualTo(pair.Settings.DummyTicker));
|
||||
|
||||
var status = ticker.PlayerGameStatuses[sPlayer.Sessions.Single().UserId];
|
||||
var expected = pair.Settings.InLobby
|
||||
? PlayerGameStatus.NotReadyToPlay
|
||||
: PlayerGameStatus.JoinedGame;
|
||||
|
||||
Assert.That(status, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
private static Pair GrabOptimalPair(PoolSettings poolSettings)
|
||||
{
|
||||
lock (PairLock)
|
||||
@@ -410,10 +437,10 @@ public static class PoolManager
|
||||
var configManager = pair.Server.ResolveDependency<IConfigurationManager>();
|
||||
var entityManager = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var gameTicker = entityManager.System<GameTicker>();
|
||||
await pair.Server.WaitPost(() =>
|
||||
{
|
||||
configManager.SetCVar(CCVars.GameLobbyEnabled, poolSettings.InLobby);
|
||||
});
|
||||
|
||||
configManager.SetCVar(CCVars.GameLobbyEnabled, poolSettings.InLobby);
|
||||
configManager.SetCVar(CCVars.GameMap, TestMap);
|
||||
|
||||
var cNetMgr = pair.Client.ResolveDependency<IClientNetManager>();
|
||||
if (!cNetMgr.IsConnected)
|
||||
{
|
||||
@@ -475,21 +502,21 @@ public static class PoolManager
|
||||
}
|
||||
}
|
||||
|
||||
configManager.SetCVar(CCVars.GameMap, poolSettings.Map);
|
||||
await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Restarting server again");
|
||||
await pair.Server.WaitPost(() =>
|
||||
{
|
||||
gameTicker.RestartRound();
|
||||
});
|
||||
|
||||
configManager.SetCVar(CCVars.GameMap, poolSettings.Map);
|
||||
configManager.SetCVar(CCVars.GameDummyTicker, poolSettings.DummyTicker);
|
||||
await pair.Server.WaitPost(() => gameTicker.RestartRound());
|
||||
|
||||
if (!poolSettings.NotConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Connecting client");
|
||||
await ReallyBeIdle(pair);
|
||||
pair.Client.SetConnectTarget(pair.Server);
|
||||
var netMgr = pair.Client.ResolveDependency<IClientNetManager>();
|
||||
await pair.Client.WaitPost(() =>
|
||||
{
|
||||
var netMgr = IoCManager.Resolve<IClientNetManager>();
|
||||
if (!netMgr.IsConnected)
|
||||
{
|
||||
netMgr.ClientConnect(null!, 0, null!);
|
||||
@@ -631,6 +658,31 @@ we are just going to end this here to save a lot of time. This is the exception
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the server/clients until the ticks are synchronized.
|
||||
/// By default the client will be one tick ahead of the server.
|
||||
/// </summary>
|
||||
public static async Task SyncTicks(Pair pair, int targetDelta = 1)
|
||||
{
|
||||
var sTiming = pair.Server.ResolveDependency<IGameTiming>();
|
||||
var cTiming = pair.Client.ResolveDependency<IGameTiming>();
|
||||
var sTick = (int)sTiming.CurTick.Value;
|
||||
var cTick = (int)cTiming.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
if (delta == targetDelta)
|
||||
return;
|
||||
if (delta > targetDelta)
|
||||
await pair.Server.WaitRunTicks(delta - targetDelta);
|
||||
else
|
||||
await pair.Client.WaitRunTicks(targetDelta - delta);
|
||||
|
||||
sTick = (int)sTiming.CurTick.Value;
|
||||
cTick = (int)cTiming.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
Assert.That(delta, Is.EqualTo(targetDelta));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a server, or a client until a condition is true
|
||||
/// </summary>
|
||||
@@ -717,12 +769,12 @@ public sealed class PoolSettings
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || DisableInterpolate || DummyTicker || NoToolsExtraPrototypes;
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || NoToolsExtraPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public bool MustBeNew => Fresh || NoLoadContent || DisableInterpolate || DummyTicker || NoToolsExtraPrototypes;
|
||||
public bool MustBeNew => Fresh || NoLoadContent || NoToolsExtraPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must not be connected
|
||||
@@ -751,6 +803,7 @@ public sealed class PoolSettings
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be in the lobby.
|
||||
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
|
||||
/// </summary>
|
||||
public bool InLobby { get; init; }
|
||||
|
||||
@@ -778,7 +831,7 @@ public sealed class PoolSettings
|
||||
/// <summary>
|
||||
/// Set this to the path of a map to have the given server/client pair load the map.
|
||||
/// </summary>
|
||||
public string Map { get; init; } // TODO for map painter
|
||||
public string Map { get; init; } = PoolManager.TestMap;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the test won't use the client (so we can skip cleaning it up)
|
||||
@@ -802,17 +855,21 @@ public sealed class PoolSettings
|
||||
/// <returns>If we can skip cleaning it up</returns>
|
||||
public bool CanFastRecycle(PoolSettings nextSettings)
|
||||
{
|
||||
if (Dirty) return false;
|
||||
if (Destructive || nextSettings.Destructive) return false;
|
||||
if (NotConnected != nextSettings.NotConnected) return false;
|
||||
if (InLobby != nextSettings.InLobby) return false;
|
||||
if (DisableInterpolate != nextSettings.DisableInterpolate) return false;
|
||||
if (nextSettings.DummyTicker) return false;
|
||||
if (Map != nextSettings.Map) return false;
|
||||
if (NoLoadContent != nextSettings.NoLoadContent) return false;
|
||||
if (nextSettings.Fresh) return false;
|
||||
if (ExtraPrototypes != nextSettings.ExtraPrototypes) return false;
|
||||
return true;
|
||||
if (MustNotBeReused)
|
||||
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
|
||||
|
||||
if (nextSettings.MustBeNew)
|
||||
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
|
||||
|
||||
if (Dirty)
|
||||
return false;
|
||||
|
||||
// Check that certain settings match.
|
||||
return NotConnected == nextSettings.NotConnected
|
||||
&& DummyTicker == nextSettings.DummyTicker
|
||||
&& Map == nextSettings.Map
|
||||
&& InLobby == nextSettings.InLobby
|
||||
&& ExtraPrototypes == nextSettings.ExtraPrototypes;
|
||||
}
|
||||
|
||||
// Prototype hot reload is not available outside TOOLS builds,
|
||||
|
||||
@@ -157,7 +157,11 @@ public sealed class SaveLoadReparentTest
|
||||
Assert.That(component.ParentSlot.Child, Is.EqualTo(id));
|
||||
});
|
||||
}
|
||||
|
||||
maps.DeleteMap(mapId);
|
||||
}
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Server.Database;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Commands
|
||||
{
|
||||
@@ -14,126 +15,140 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
[Test]
|
||||
public async Task PardonTest()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new() { Destructive = true });
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var server = pairTracker.Pair.Server;
|
||||
var client = pairTracker.Pair.Client;
|
||||
|
||||
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
|
||||
var sConsole = server.ResolveDependency<IServerConsoleHost>();
|
||||
var sDatabase = server.ResolveDependency<IServerDbManager>();
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
var clientSession = sPlayerManager.Sessions.Single();
|
||||
var clientId = clientSession.UserId;
|
||||
|
||||
await server.WaitAssertion(async () =>
|
||||
Assert.That(netMan.IsConnected);
|
||||
|
||||
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
|
||||
// No bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
var clientSession = sPlayerManager.Sessions.Single();
|
||||
var clientId = clientSession.UserId;
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
|
||||
});
|
||||
|
||||
// No bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
|
||||
});
|
||||
// Try to pardon a ban that does not exist
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
|
||||
|
||||
// Try to pardon a ban that does not exist
|
||||
sConsole.ExecuteCommand("pardon 1");
|
||||
// Still no bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
|
||||
});
|
||||
|
||||
// Still no bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
|
||||
});
|
||||
var banReason = "test";
|
||||
|
||||
var banReason = "test";
|
||||
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
|
||||
// Ban the client for 24 hours
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand($"ban {clientSession.Name} {banReason} 1440"));
|
||||
|
||||
// Ban the client for 24 hours
|
||||
sConsole.ExecuteCommand($"ban {clientSession.Name} {banReason} 1440");
|
||||
|
||||
// Should have one ban on record now
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
|
||||
// Try to pardon a ban that does not exist
|
||||
sConsole.ExecuteCommand("pardon 2");
|
||||
|
||||
// The existing ban is unaffected
|
||||
// Should have one ban on record now
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
|
||||
var ban = await sDatabase.GetServerBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(ban, Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(0));
|
||||
Assert.That(!netMan.IsConnected);
|
||||
|
||||
// Check that it matches
|
||||
Assert.That(ban.Id, Is.EqualTo(1));
|
||||
Assert.That(ban.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(ban.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(ban.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(ban.Reason, Is.EqualTo(banReason));
|
||||
// Try to pardon a ban that does not exist
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(ban.BanningAdmin, Is.Null);
|
||||
Assert.That(ban.Unban, Is.Null);
|
||||
});
|
||||
// The existing ban is unaffected
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
|
||||
|
||||
// Pardon the actual ban
|
||||
sConsole.ExecuteCommand("pardon 1");
|
||||
var ban = await sDatabase.GetServerBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(ban, Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
|
||||
// Check that it matches
|
||||
Assert.That(ban.Id, Is.EqualTo(1));
|
||||
Assert.That(ban.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(ban.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(ban.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(ban.Reason, Is.EqualTo(banReason));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(ban.BanningAdmin, Is.Null);
|
||||
Assert.That(ban.Unban, Is.Null);
|
||||
});
|
||||
|
||||
// Pardon the actual ban
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
|
||||
|
||||
// No bans should be returned
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
|
||||
// Direct id lookup returns a pardoned ban
|
||||
var pardonedBan = await sDatabase.GetServerBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// Check that it matches
|
||||
Assert.That(pardonedBan, Is.Not.Null);
|
||||
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
|
||||
Assert.That(pardonedBan.Id, Is.EqualTo(1));
|
||||
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(pardonedBan.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(pardonedBan.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(pardonedBan.Reason, Is.EqualTo(banReason));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(pardonedBan.BanningAdmin, Is.Null);
|
||||
|
||||
Assert.That(pardonedBan.Unban, Is.Not.Null);
|
||||
Assert.That(pardonedBan.Unban.BanId, Is.EqualTo(1));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(pardonedBan.Unban.UnbanningAdmin, Is.Null);
|
||||
|
||||
Assert.That(pardonedBan.Unban.UnbanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
});
|
||||
|
||||
// Try to pardon it again
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
|
||||
|
||||
// Nothing changes
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// No bans should be returned
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
|
||||
// Direct id lookup returns a pardoned ban
|
||||
var pardonedBan = await sDatabase.GetServerBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// Check that it matches
|
||||
Assert.That(pardonedBan, Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
|
||||
Assert.That(pardonedBan.Id, Is.EqualTo(1));
|
||||
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(pardonedBan.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(pardonedBan.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(pardonedBan.Reason, Is.EqualTo(banReason));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(pardonedBan.BanningAdmin, Is.Null);
|
||||
|
||||
Assert.That(pardonedBan.Unban, Is.Not.Null);
|
||||
Assert.That(pardonedBan.Unban.BanId, Is.EqualTo(1));
|
||||
|
||||
// Done through the console
|
||||
Assert.That(pardonedBan.Unban.UnbanningAdmin, Is.Null);
|
||||
|
||||
Assert.That(pardonedBan.Unban.UnbanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
});
|
||||
|
||||
// Try to pardon it again
|
||||
sConsole.ExecuteCommand("pardon 1");
|
||||
|
||||
// Nothing changes
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// No bans should be returned
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
|
||||
|
||||
// Direct id lookup returns a pardoned ban
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
|
||||
// Reconnect client. Slightly faster than dirtying the pair.
|
||||
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(0));
|
||||
client.SetConnectTarget(server);
|
||||
await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!));
|
||||
await PoolManager.ReallyBeIdle(pairTracker.Pair);
|
||||
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ namespace Content.IntegrationTests.Tests
|
||||
[Test]
|
||||
public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, Destructive = true });
|
||||
// This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round
|
||||
// is minimal relative to the rest of the test.
|
||||
var settings = new PoolSettings {NoClient = true, Dirty = true};
|
||||
await using var pairTracker = await PoolManager.GetServerClient(settings);
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entityMan = server.ResolveDependency<IEntityManager>();
|
||||
@@ -71,7 +74,10 @@ namespace Content.IntegrationTests.Tests
|
||||
[Test]
|
||||
public async Task SpawnAndDeleteAllEntitiesInTheSameSpot()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, Destructive = true });
|
||||
// This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round
|
||||
// is minimal relative to the rest of the test.
|
||||
var settings = new PoolSettings {NoClient = true, Dirty = true};
|
||||
await using var pairTracker = await PoolManager.GetServerClient(settings);
|
||||
var server = pairTracker.Pair.Server;
|
||||
var map = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
@@ -123,7 +129,10 @@ namespace Content.IntegrationTests.Tests
|
||||
[Test]
|
||||
public async Task SpawnAndDirtyAllEntities()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = false, Destructive = true });
|
||||
// This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round
|
||||
// is minimal relative to the rest of the test.
|
||||
var settings = new PoolSettings {NoClient = false, Dirty = true};
|
||||
await using var pairTracker = await PoolManager.GetServerClient(settings);
|
||||
var server = pairTracker.Pair.Server;
|
||||
var client = pairTracker.Pair.Client;
|
||||
|
||||
@@ -211,11 +220,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"BiomeSelection", // Whaddya know, requires config.
|
||||
};
|
||||
|
||||
var testEntity = @"
|
||||
- type: entity
|
||||
id: AllComponentsOneToOneDeleteTestEntity";
|
||||
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, ExtraPrototypes = testEntity });
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
@@ -263,7 +268,7 @@ namespace Content.IntegrationTests.Tests
|
||||
continue;
|
||||
}
|
||||
|
||||
var entity = entityManager.SpawnEntity("AllComponentsOneToOneDeleteTestEntity", testLocation);
|
||||
var entity = entityManager.SpawnEntity(null, testLocation);
|
||||
|
||||
Assert.That(entityManager.GetComponent<MetaDataComponent>(entity).EntityInitialized);
|
||||
|
||||
@@ -271,6 +276,7 @@ namespace Content.IntegrationTests.Tests
|
||||
// such as MetaData or Transform
|
||||
if (entityManager.HasComponent(entity, type))
|
||||
{
|
||||
entityManager.DeleteEntity(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -311,11 +317,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"BiomeSelection", // Whaddya know, requires config.
|
||||
};
|
||||
|
||||
var testEntity = @"
|
||||
- type: entity
|
||||
id: AllComponentsOneEntityDeleteTestEntity";
|
||||
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, ExtraPrototypes = testEntity });
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
@@ -384,7 +386,7 @@ namespace Content.IntegrationTests.Tests
|
||||
foreach (var (components, _) in distinctComponents)
|
||||
{
|
||||
var testLocation = grid.ToCoordinates();
|
||||
var entity = entityManager.SpawnEntity("AllComponentsOneEntityDeleteTestEntity", testLocation);
|
||||
var entity = entityManager.SpawnEntity(null, testLocation);
|
||||
|
||||
Assert.That(entityManager.GetComponent<MetaDataComponent>(entity).EntityInitialized);
|
||||
|
||||
|
||||
@@ -28,5 +28,7 @@ public sealed class LogErrorTest
|
||||
// But errors do
|
||||
await server.WaitPost(() => Assert.Throws<AssertionException>(() => logmill.Error("test")));
|
||||
await client.WaitPost(() => Assert.Throws<AssertionException>(() => logmill.Error("test")));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ public sealed class NPCTest
|
||||
{
|
||||
var counts = new Dictionary<string, int>();
|
||||
|
||||
foreach (var compound in protoManager.EnumeratePrototypes<HTNCompoundTask>())
|
||||
foreach (var compound in protoManager.EnumeratePrototypes<HTNCompoundPrototype>())
|
||||
{
|
||||
Count(compound, counts, htnSystem);
|
||||
Count(compound, counts, htnSystem, protoManager);
|
||||
counts.Clear();
|
||||
}
|
||||
});
|
||||
@@ -34,13 +34,11 @@ public sealed class NPCTest
|
||||
await pool.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private static void Count(HTNCompoundTask compound, Dictionary<string, int> counts, HTNSystem htnSystem)
|
||||
private static void Count(HTNCompoundPrototype compound, Dictionary<string, int> counts, HTNSystem htnSystem, IPrototypeManager protoManager)
|
||||
{
|
||||
var compoundBranches = htnSystem.CompoundBranches[compound];
|
||||
|
||||
for (var i = 0; i < compound.Branches.Count; i++)
|
||||
foreach (var branch in compound.Branches)
|
||||
{
|
||||
foreach (var task in compoundBranches[i])
|
||||
foreach (var task in branch.Tasks)
|
||||
{
|
||||
if (task is HTNCompoundTask compoundTask)
|
||||
{
|
||||
@@ -49,7 +47,7 @@ public sealed class NPCTest
|
||||
|
||||
Assert.That(count, Is.LessThan(50));
|
||||
counts[compound.ID] = count;
|
||||
Count(compoundTask, counts, htnSystem);
|
||||
Count(protoManager.Index<HTNCompoundPrototype>(compoundTask.Task), counts, htnSystem, protoManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Networking
|
||||
@@ -35,6 +33,8 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
// TODO remove fresh=true.
|
||||
// Instead, offset the all the explicit tick checks by some initial tick number.
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var client = pairTracker.Pair.Client;
|
||||
@@ -390,7 +390,6 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Reflect(false)]
|
||||
public sealed class AutoPredictionTestEntitySystem : EntitySystem
|
||||
{
|
||||
public bool Allow { get; set; } = true;
|
||||
@@ -446,6 +445,7 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
[NetworkedComponent()]
|
||||
[AutoGenerateComponentState]
|
||||
[Access(typeof(AutoPredictReconcileTest.AutoPredictionTestEntitySystem))]
|
||||
[RegisterComponent]
|
||||
public sealed partial class AutoPredictionTestComponent : Component
|
||||
{
|
||||
[AutoNetworkedField]
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
|
||||
Assert.That(clEntityManager.GetComponent<TransformComponent>(lastSvEntity).Coordinates,
|
||||
Is.EqualTo(svEntityManager.GetComponent<TransformComponent>(lastSvEntity).Coordinates));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -36,6 +35,8 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
// TODO remove fresh=true.
|
||||
// Instead, offset the all the explicit tick checks by some initial tick number.
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var client = pairTracker.Pair.Client;
|
||||
@@ -393,12 +394,12 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
|
||||
[NetworkedComponent()]
|
||||
[Access(typeof(PredictionTestEntitySystem))]
|
||||
[RegisterComponent]
|
||||
public sealed class PredictionTestComponent : Component
|
||||
{
|
||||
public bool Foo;
|
||||
}
|
||||
|
||||
[Reflect(false)]
|
||||
public sealed class PredictionTestEntitySystem : EntitySystem
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -12,7 +12,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -35,6 +34,8 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
// TODO remove fresh=true.
|
||||
// Instead, offset the all the explicit tick checks by some initial tick number.
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new() { Fresh = true, DummyTicker = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var client = pairTracker.Pair.Client;
|
||||
@@ -392,12 +393,12 @@ namespace Content.IntegrationTests.Tests.Networking
|
||||
|
||||
[NetworkedComponent()]
|
||||
[Access(typeof(SystemPredictionTestEntitySystem))]
|
||||
[RegisterComponent]
|
||||
public sealed class SystemPredictionTestComponent : Component
|
||||
{
|
||||
public bool Foo;
|
||||
}
|
||||
|
||||
[Reflect(false)]
|
||||
public sealed class SystemPredictionTestEntitySystem : EntitySystem
|
||||
{
|
||||
public bool Allow { get; set; } = true;
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace Content.IntegrationTests.Tests
|
||||
var mapNames = new List<string>();
|
||||
var naughty = new HashSet<string>()
|
||||
{
|
||||
"Empty",
|
||||
PoolManager.TestMap,
|
||||
"Infiltrator",
|
||||
"Pirate",
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class PrototypeSaveTest
|
||||
public async Task UninitializedSaveTest()
|
||||
{
|
||||
// Apparently SpawnTest fails to clean up properly. Due to the similarities, I'll assume this also fails.
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, Dirty = true, Destructive = true });
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
|
||||
@@ -13,7 +13,7 @@ using Robust.Shared.Utility;
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that the
|
||||
/// Tests that a map's yaml does not change when saved consecutively.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class SaveLoadSaveTest
|
||||
@@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests
|
||||
[Test]
|
||||
public async Task SaveLoadSave()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { Fresh = true, Disconnected = true });
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapLoader = entManager.System<MapLoaderSystem>();
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Serialization;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SerializationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that serializing generic enums works as intended. This should really be in engine, but engine
|
||||
/// integrations tests block reflection and I am lazy..
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SerializeGenericEnums()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var seriMan = server.ResolveDependency<ISerializationManager>();
|
||||
var refMan = server.ResolveDependency<IReflectionManager>();
|
||||
|
||||
Enum value = TestEnum.Bb;
|
||||
|
||||
var node = seriMan.WriteValue(value, notNullableOverride:true);
|
||||
var valueNode = node as ValueDataNode;
|
||||
Assert.NotNull(valueNode);
|
||||
|
||||
var expected = refMan.GetEnumReference(value);
|
||||
Assert.That(valueNode!.Value, Is.EqualTo(expected));
|
||||
|
||||
var errors = seriMan.ValidateNode<Enum>(valueNode).GetErrors();
|
||||
Assert.That(errors.Any(), Is.False);
|
||||
|
||||
var deserialized = seriMan.Read<Enum>(node, notNullableOverride:true);
|
||||
Assert.That(deserialized, Is.EqualTo(value));
|
||||
|
||||
// Repeat test with enums in a data definitions.
|
||||
var data = new TestData
|
||||
{
|
||||
Value = TestEnum.Cc,
|
||||
Sequence = new() {TestEnum.Dd, TestEnum.Aa}
|
||||
};
|
||||
|
||||
node = seriMan.WriteValue(data, notNullableOverride:true);
|
||||
|
||||
errors = seriMan.ValidateNode<TestData>(node).GetErrors();
|
||||
Assert.That(errors.Any(), Is.False);
|
||||
|
||||
var deserializedData = seriMan.Read<TestData>(node, notNullableOverride:false);
|
||||
|
||||
Assert.That(deserializedData.Value, Is.EqualTo(data.Value));
|
||||
Assert.That(deserializedData.Sequence.Count, Is.EqualTo(data.Sequence.Count));
|
||||
Assert.That(deserializedData.Sequence[0], Is.EqualTo(data.Sequence[0]));
|
||||
Assert.That(deserializedData.Sequence[1], Is.EqualTo(data.Sequence[1]));
|
||||
|
||||
// Check that Generic & non-generic serializers are incompativle.
|
||||
Enum genericValue = TestEnum.Bb;
|
||||
TestEnum typedValue = TestEnum.Bb;
|
||||
|
||||
var genericNode = seriMan.WriteValue(genericValue, notNullableOverride:true);
|
||||
var typedNode = seriMan.WriteValue(typedValue);
|
||||
|
||||
Assert.That(seriMan.ValidateNode<Enum>(genericNode).GetErrors().Any(), Is.False);
|
||||
Assert.That(seriMan.ValidateNode<TestEnum>(genericNode).GetErrors().Any(), Is.True);
|
||||
Assert.That(seriMan.ValidateNode<Enum>(typedNode).GetErrors().Any(), Is.True);
|
||||
Assert.That(seriMan.ValidateNode<TestEnum>(typedNode).GetErrors().Any(), Is.False);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private enum TestEnum : byte { Aa, Bb, Cc, Dd }
|
||||
|
||||
[DataDefinition]
|
||||
private sealed class TestData
|
||||
{
|
||||
[DataField("value")] public Enum Value = default!;
|
||||
[DataField("sequence")] public List<Enum> Sequence = default!;
|
||||
}
|
||||
}
|
||||
1767
Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs
generated
Normal file
1767
Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminLogCompoundKey : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "admin_log_id",
|
||||
table: "admin_log",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer")
|
||||
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "admin_log_id",
|
||||
table: "admin_log",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,12 +85,9 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("admin_log_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("RoundId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
1697
Content.Server.Database/Migrations/Sqlite/20230727190858_AdminLogCompoundKey.Designer.cs
generated
Normal file
1697
Content.Server.Database/Migrations/Sqlite/20230727190858_AdminLogCompoundKey.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminLogCompoundKey : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("admin_log_id");
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ namespace Content.Server.Database
|
||||
.HasKey(log => new {log.Id, log.RoundId});
|
||||
|
||||
modelBuilder.Entity<AdminLog>()
|
||||
.Property(log => log.Id)
|
||||
.ValueGeneratedOnAdd();
|
||||
.Property(log => log.Id);
|
||||
|
||||
modelBuilder.Entity<AdminLog>()
|
||||
.HasIndex(log => log.Date);
|
||||
@@ -492,7 +491,7 @@ namespace Content.Server.Database
|
||||
[Index(nameof(Type))]
|
||||
public class AdminLog
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Key, ForeignKey("Round")] public int RoundId { get; set; }
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Server.Access.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class AgentIDCardComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of job icons that the agent ID card can show.
|
||||
/// </summary>
|
||||
[DataField("icons", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StatusIconPrototype>))]
|
||||
public readonly HashSet<string> Icons = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using Content.Server.UserInterface;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Access.Systems
|
||||
{
|
||||
@@ -13,6 +15,7 @@ namespace Content.Server.Access.Systems
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IdCardSystem _cardSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -22,6 +25,7 @@ namespace Content.Server.Access.Systems
|
||||
SubscribeLocalEvent<AgentIDCardComponent, AfterActivatableUIOpenEvent>(AfterUIOpen);
|
||||
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged);
|
||||
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged);
|
||||
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobIconChangedMessage>(OnJobIconChanged);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
|
||||
@@ -61,7 +65,7 @@ namespace Content.Server.Access.Systems
|
||||
if (!TryComp<IdCardComponent>(uid, out var idCard))
|
||||
return;
|
||||
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "");
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", component.Icons);
|
||||
UserInterfaceSystem.SetUiState(ui, state, args.Session);
|
||||
}
|
||||
|
||||
@@ -80,5 +84,20 @@ namespace Content.Server.Access.Systems
|
||||
|
||||
_cardSystem.TryChangeFullName(uid, args.Name, idCard);
|
||||
}
|
||||
|
||||
private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobIconChangedMessage args)
|
||||
{
|
||||
if (!TryComp<IdCardComponent>(uid, out var idCard))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.TryIndex<StatusIconPrototype>(args.JobIcon, out var jobIcon))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.Access.Components;
|
||||
@@ -7,10 +6,12 @@ using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StationRecords;
|
||||
using Content.Shared.StatusIcon;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using static Content.Shared.Access.Components.IdCardConsoleComponent;
|
||||
|
||||
namespace Content.Server.Access.Systems;
|
||||
@@ -129,6 +130,12 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
_idCard.TryChangeFullName(targetId, newFullName, player: player);
|
||||
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
|
||||
|
||||
if (_prototype.TryIndex<JobPrototype>(newJobProto, out var job)
|
||||
&& _prototype.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
|
||||
{
|
||||
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
|
||||
}
|
||||
|
||||
if (!newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
|
||||
{
|
||||
_sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag.");
|
||||
@@ -145,7 +152,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
|
||||
// I hate that C# doesn't have an option for this and don't desire to write this out the hard way.
|
||||
// var difference = newAccessList.Difference(oldTags);
|
||||
var difference = (newAccessList.Union(oldTags)).Except(newAccessList.Intersect(oldTags)).ToHashSet();
|
||||
var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet();
|
||||
// NULL SAFETY: PrivilegedIdIsAuthorized checked this earlier.
|
||||
var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet();
|
||||
if (!difference.IsSubsetOf(privilegedPerms))
|
||||
@@ -163,7 +170,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||
|
||||
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, newJobProto);
|
||||
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -184,7 +191,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, reader);
|
||||
}
|
||||
|
||||
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, string newJobProto)
|
||||
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
|
||||
{
|
||||
if (_station.GetOwningStation(uid) is not { } station
|
||||
|| !EntityManager.TryGetComponent<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
@@ -197,10 +204,10 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
record.Name = newFullName;
|
||||
record.JobTitle = newJobTitle;
|
||||
|
||||
if (_prototype.TryIndex<JobPrototype>(newJobProto, out var job))
|
||||
if (newJobProto != null)
|
||||
{
|
||||
record.JobPrototype = newJobProto;
|
||||
record.JobIcon = job.Icon;
|
||||
record.JobPrototype = newJobProto.ID;
|
||||
record.JobIcon = newJobProto.Icon;
|
||||
}
|
||||
|
||||
_record.Synchronize(station);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Popups;
|
||||
@@ -7,8 +6,10 @@ using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Access.Systems
|
||||
{
|
||||
@@ -118,6 +119,30 @@ namespace Content.Server.Access.Systems
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null)
|
||||
{
|
||||
if (!Resolve(uid, ref id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id.JobIcon == jobIcon.ID)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
id.JobIcon = jobIcon.ID;
|
||||
Dirty(id);
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
_adminLogger.Add(LogType.Identity, LogImpact.Low,
|
||||
$"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(id.Owner):entity} to {jobIcon} ");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to change the full name of a card.
|
||||
/// Returns true/false.
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Access.Systems
|
||||
@@ -67,8 +68,12 @@ namespace Content.Server.Access.Systems
|
||||
|
||||
_accessSystem.SetAccessToJob(uid, job, extended);
|
||||
|
||||
// and also change job title on a card id
|
||||
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
|
||||
|
||||
if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
|
||||
{
|
||||
_cardSystem.TryChangeJobIcon(uid, jobIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,8 @@ namespace Content.Server.Administration.Commands
|
||||
|
||||
var i = 0;
|
||||
|
||||
foreach (var component in components)
|
||||
foreach (var (uid, component) in components)
|
||||
{
|
||||
var uid = component.Owner;
|
||||
entityManager.RemoveComponent(uid, component);
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Content.Server.Administration.Commands
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Owner));
|
||||
var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Uid));
|
||||
var entitiesWithAllComponents = entitiesWithComponents.Skip(1).Aggregate(new HashSet<EntityUid>(entitiesWithComponents.First()), (h, e) => { h.IntersectWith(e); return h; });
|
||||
|
||||
var count = 0;
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Content.Server.Administration.Commands
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var entityIds = new HashSet<string>();
|
||||
|
||||
var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Owner)).ToArray();
|
||||
var entitiesWithComponents = components.Select(c => entityManager.GetAllComponents(c).Select(x => x.Uid)).ToArray();
|
||||
var entitiesWithAllComponents = entitiesWithComponents.Skip(1).Aggregate(new HashSet<EntityUid>(entitiesWithComponents.First()), (h, e) => { h.IntersectWith(e); return h; });
|
||||
|
||||
foreach (var entity in entitiesWithAllComponents)
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Players;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -151,9 +152,18 @@ public sealed class BanManager : IBanManager, IPostInjectInit
|
||||
: "null";
|
||||
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
|
||||
|
||||
var logMessage = Loc.GetString("server-ban-string", ("admin", adminName), ("severity", severity),
|
||||
("expires", expiresString), ("name", targetName), ("ip", addressRangeString),
|
||||
("hwid", hwidString), ("reason", reason));
|
||||
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
|
||||
|
||||
var logMessage = Loc.GetString(
|
||||
key,
|
||||
("admin", adminName),
|
||||
("severity", severity),
|
||||
("expires", expiresString),
|
||||
("name", targetName),
|
||||
("ip", addressRangeString),
|
||||
("hwid", hwidString),
|
||||
("reason", reason));
|
||||
|
||||
_sawmill.Info(logMessage);
|
||||
_chat.SendAdminAlert(logMessage);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Content.Server.Advertise
|
||||
/// <summary>
|
||||
/// The identifier for the advertisements pack prototype.
|
||||
/// </summary>
|
||||
[DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer<AdvertisementsPackPrototype>))]
|
||||
[DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer<AdvertisementsPackPrototype>), required: true)]
|
||||
public string PackPrototypeId { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Shared.Tools;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Ame.Components;
|
||||
@@ -19,6 +19,6 @@ public sealed class AmePartComponent : Component
|
||||
/// <summary>
|
||||
/// The tool quality required to deploy the packaged AME shielding.
|
||||
/// </summary>
|
||||
[DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
[DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
|
||||
public string QualityNeeded = "Pulsing";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Materials;
|
||||
@@ -93,8 +94,7 @@ public sealed partial class AnomalySystem
|
||||
var xform = Transform(grid);
|
||||
|
||||
var targetCoords = xform.Coordinates;
|
||||
var gridBounds = gridComp.LocalAABB;
|
||||
gridBounds.Scale(_configuration.GetCVar(CCVars.AnomalyGenerationGridBoundsScale));
|
||||
var gridBounds = gridComp.LocalAABB.Scale(_configuration.GetCVar(CCVars.AnomalyGenerationGridBoundsScale));
|
||||
|
||||
for (var i = 0; i < 25; i++)
|
||||
{
|
||||
@@ -147,11 +147,18 @@ public sealed partial class AnomalySystem
|
||||
|
||||
private void OnGeneratingFinished(EntityUid uid, AnomalyGeneratorComponent component)
|
||||
{
|
||||
var grid = Transform(uid).GridUid;
|
||||
if (grid == null)
|
||||
return;
|
||||
var xform = Transform(uid);
|
||||
|
||||
SpawnOnRandomGridLocation(grid.Value, component.SpawnerPrototype);
|
||||
if (_station.GetStationInMap(xform.MapID) is not { } station ||
|
||||
!TryComp<StationDataComponent>(station, out var data) ||
|
||||
_station.GetLargestGrid(data) is not { } grid)
|
||||
{
|
||||
if (xform.GridUid == null)
|
||||
return;
|
||||
grid = xform.GridUid.Value;
|
||||
}
|
||||
|
||||
SpawnOnRandomGridLocation(grid, component.SpawnerPrototype);
|
||||
RemComp<GeneratingAnomalyGeneratorComponent>(uid);
|
||||
Appearance.SetData(uid, AnomalyGeneratorVisuals.Generating, false);
|
||||
Audio.PlayPvs(component.GeneratingFinishedSound, uid);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Audio;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -28,6 +29,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Content.Server.Atmos.Monitor.Systems;
|
||||
public sealed class AirAlarmSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
|
||||
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
|
||||
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNetSystem = default!;
|
||||
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
@@ -290,10 +291,15 @@ public sealed class AirAlarmSystem : EntitySystem
|
||||
|
||||
private void OnUpdateDeviceData(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateDeviceDataMessage args)
|
||||
{
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component))
|
||||
if (AccessCheck(uid, args.Session.AttachedEntity, component)
|
||||
&& _deviceListSystem.ExistsInDeviceList(uid, args.Address))
|
||||
{
|
||||
SetDeviceData(uid, args.Address, args.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUI(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
private bool AccessCheck(EntityUid uid, EntityUid? user, AirAlarmComponent? component = null)
|
||||
|
||||
@@ -35,29 +35,15 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
if (!_nodeContainer.TryGetNode(nodeContainer, vent.InletName, out PipeNode? inlet))
|
||||
return;
|
||||
|
||||
var environmentPressure = environment.Pressure;
|
||||
var pressureDelta = MathF.Abs(environmentPressure - inlet.Air.Pressure);
|
||||
var inletAir = inlet.Air.RemoveRatio(1f);
|
||||
var envAir = environment.RemoveRatio(1f);
|
||||
|
||||
if ((environment.Temperature > 0 || inlet.Air.Temperature > 0) && pressureDelta > 0.5f)
|
||||
{
|
||||
if (environmentPressure < inlet.Air.Pressure)
|
||||
{
|
||||
var airTemperature = environment.Temperature > 0 ? environment.Temperature : inlet.Air.Temperature;
|
||||
var transferMoles = pressureDelta * environment.Volume / (airTemperature * Atmospherics.R);
|
||||
var removed = inlet.Air.Remove(transferMoles);
|
||||
_atmosphereSystem.Merge(environment, removed);
|
||||
}
|
||||
else
|
||||
{
|
||||
var airTemperature = inlet.Air.Temperature > 0 ? inlet.Air.Temperature : environment.Temperature;
|
||||
var outputVolume = inlet.Air.Volume;
|
||||
var transferMoles = (pressureDelta * outputVolume) / (airTemperature * Atmospherics.R);
|
||||
transferMoles = MathF.Min(transferMoles, environment.TotalMoles * inlet.Air.Volume / environment.Volume);
|
||||
var removed = environment.Remove(transferMoles);
|
||||
_atmosphereSystem.Merge(inlet.Air, removed);
|
||||
}
|
||||
}
|
||||
var mergeAir = new GasMixture(inletAir.Volume + envAir.Volume);
|
||||
_atmosphereSystem.Merge(mergeAir, inletAir);
|
||||
_atmosphereSystem.Merge(mergeAir, envAir);
|
||||
|
||||
_atmosphereSystem.Merge(inlet.Air, mergeAir.RemoveVolume(inletAir.Volume));
|
||||
_atmosphereSystem.Merge(environment, mergeAir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ using Content.Server.GameTicking;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Body.Systems;
|
||||
@@ -36,11 +36,74 @@ public sealed class BodySystem : SharedBodySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BodyPartComponent, ComponentStartup>(OnPartStartup);
|
||||
SubscribeLocalEvent<BodyComponent, ComponentStartup>(OnBodyStartup);
|
||||
SubscribeLocalEvent<BodyComponent, MoveInputEvent>(OnRelayMoveInput);
|
||||
SubscribeLocalEvent<BodyComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
SubscribeLocalEvent<BodyComponent, BeingMicrowavedEvent>(OnBeingMicrowaved);
|
||||
}
|
||||
|
||||
private void OnPartStartup(EntityUid uid, BodyPartComponent component, ComponentStartup args)
|
||||
{
|
||||
// This inter-entity relationship makes be deeply uncomfortable because its probably going to re-encounter
|
||||
// all of the networking & startup ordering issues that containers and joints have.
|
||||
// TODO just use containers. Please.
|
||||
|
||||
foreach (var slot in component.Children.Values)
|
||||
{
|
||||
DebugTools.Assert(slot.Parent == uid);
|
||||
if (slot.Child == null)
|
||||
continue;
|
||||
|
||||
if (TryComp(slot.Child, out BodyPartComponent? child))
|
||||
{
|
||||
child.ParentSlot = slot;
|
||||
Dirty(slot.Child.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Error($"Body part encountered missing limbs: {ToPrettyString(uid)}. Slot: {slot.Id}");
|
||||
slot.Child = null;
|
||||
}
|
||||
|
||||
foreach (var slot in component.Organs.Values)
|
||||
{
|
||||
DebugTools.Assert(slot.Parent == uid);
|
||||
if (slot.Child == null)
|
||||
continue;
|
||||
|
||||
if (TryComp(slot.Child, out OrganComponent? child))
|
||||
{
|
||||
child.ParentSlot = slot;
|
||||
Dirty(slot.Child.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Error($"Body part encountered missing organ: {ToPrettyString(uid)}. Slot: {slot.Id}");
|
||||
slot.Child = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBodyStartup(EntityUid uid, BodyComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.Root is not { } slot)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(slot.Parent == uid);
|
||||
if (slot.Child == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(slot.Child, out BodyPartComponent? child))
|
||||
{
|
||||
Log.Error($"Body part encountered missing limbs: {ToPrettyString(uid)}. Slot: {slot.Id}");
|
||||
slot.Child = null;
|
||||
return;
|
||||
}
|
||||
|
||||
child.ParentSlot = slot;
|
||||
Dirty(slot.Child.Value);
|
||||
}
|
||||
|
||||
private void OnRelayMoveInput(EntityUid uid, BodyComponent component, ref MoveInputEvent args)
|
||||
{
|
||||
if (_mobState.IsDead(uid) && _mindSystem.TryGetMind(uid, out var mind))
|
||||
|
||||
@@ -14,6 +14,8 @@ using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Radio;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -50,9 +52,11 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public const int VoiceRange = 10; // how far voice goes in world units
|
||||
public const int WhisperRange = 2; // how far whisper goes in world units
|
||||
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
|
||||
public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
|
||||
public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg";
|
||||
public const string CentComAnnouncementSound = "/Audio/Corvax/Announcements/centcomm.ogg"; // Corvax-Announcements
|
||||
|
||||
@@ -380,7 +384,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
|
||||
|
||||
// get the entity's apparent name (if no override provided).
|
||||
// get the entity's name by visual identity (if no override provided).
|
||||
string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
|
||||
// get the entity's name by voice (if no override provided).
|
||||
string name;
|
||||
if (nameOverride != null)
|
||||
{
|
||||
@@ -401,23 +407,33 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
||||
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
|
||||
var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
||||
("entityName", name), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
|
||||
("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
|
||||
|
||||
var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
|
||||
("message", FormattedMessage.EscapeText(obfuscatedMessage)));
|
||||
|
||||
|
||||
foreach (var (session, data) in GetRecipients(source, VoiceRange))
|
||||
foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
|
||||
{
|
||||
EntityUid listener;
|
||||
|
||||
if (session.AttachedEntity is not { Valid: true } playerEntity)
|
||||
continue;
|
||||
listener = session.AttachedEntity.Value;
|
||||
|
||||
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
|
||||
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
|
||||
|
||||
if (data.Range <= WhisperRange)
|
||||
if (data.Range <= WhisperClearRange)
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.ConnectedClient);
|
||||
else
|
||||
//If listener is too far, they only hear fragments of the message
|
||||
//Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind
|
||||
else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) //Shared.Physics.CollisionGroup.Opaque
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.ConnectedClient);
|
||||
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
|
||||
else
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.ConnectedClient);
|
||||
}
|
||||
|
||||
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, MessageRangeHideChatForReplay(range)));
|
||||
@@ -658,7 +674,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
/// <summary>
|
||||
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
|
||||
/// </summary>
|
||||
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceRange)
|
||||
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceGetRange)
|
||||
{
|
||||
// TODO proper speech occlusion
|
||||
|
||||
@@ -683,7 +699,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
var observer = ghosts.HasComponent(playerEntity);
|
||||
|
||||
// even if they are an observer, in some situations we still need the range
|
||||
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceRange)
|
||||
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceGetRange)
|
||||
{
|
||||
recipients.Add(player, new ICChatRecipientData(distance, observer));
|
||||
continue;
|
||||
@@ -693,7 +709,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
recipients.Add(player, new ICChatRecipientData(-1, true));
|
||||
}
|
||||
|
||||
RaiseLocalEvent(new ExpandICChatRecipientstEvent(source, voiceRange, recipients));
|
||||
RaiseLocalEvent(new ExpandICChatRecipientstEvent(source, voiceGetRange, recipients));
|
||||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Random;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
@@ -18,14 +17,8 @@ public sealed class RandomFillSolutionComponent : Component
|
||||
public string Solution { get; set; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Weighted random prototype Id. Used to pick reagent.
|
||||
/// Weighted random fill prototype Id. Used to pick reagent and quantity.
|
||||
/// </summary>
|
||||
[DataField("weightedRandomId", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
|
||||
[DataField("weightedRandomId", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomFillSolutionPrototype>))]
|
||||
public string WeightedRandomId { get; set; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Amount of reagent to add.
|
||||
/// </summary>
|
||||
[DataField("quantity")]
|
||||
public FixedPoint2 Quantity { get; set; } = 0;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Examine;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -41,6 +43,7 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ExamineSystem _examine = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -48,6 +51,7 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentInit>(InitSolution);
|
||||
SubscribeLocalEvent<ExaminableSolutionComponent, ExaminedEvent>(OnExamineSolution);
|
||||
SubscribeLocalEvent<ExaminableSolutionComponent, GetVerbsEvent<ExamineVerb>>(OnSolutionExaminableVerb);
|
||||
}
|
||||
|
||||
private void InitSolution(EntityUid uid, SolutionContainerManagerComponent component, ComponentInit args)
|
||||
@@ -60,6 +64,69 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionExaminableVerb(EntityUid uid, ExaminableSolutionComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
var scanEvent = new SolutionScanEvent();
|
||||
RaiseLocalEvent(args.User, scanEvent);
|
||||
if (!scanEvent.CanScan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SolutionContainerManagerComponent? solutionsManager = null;
|
||||
if (!Resolve(args.Target, ref solutionsManager)
|
||||
|| !solutionsManager.Solutions.TryGetValue(component.Solution, out var solutionHolder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var verb = new ExamineVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
var markup = GetSolutionExamine(solutionHolder);
|
||||
_examine.SendExamineTooltip(args.User, uid, markup, false, false);
|
||||
},
|
||||
Text = Loc.GetString("scannable-solution-verb-text"),
|
||||
Message = Loc.GetString("scannable-solution-verb-message"),
|
||||
Category = VerbCategory.Examine,
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private FormattedMessage GetSolutionExamine(Solution solution)
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
|
||||
if (solution.Contents.Count == 0) //TODO: better way to see if empty?
|
||||
{
|
||||
msg.AddMarkup(Loc.GetString("scannable-solution-empty-container"));
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.AddMarkup(Loc.GetString("scannable-solution-main-text"));
|
||||
|
||||
foreach (var reagent in solution)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.ReagentId, out var proto))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("scannable-solution-chemical"
|
||||
, ("type", proto.LocalizedName)
|
||||
, ("color", proto.SubstanceColor.ToHexNoAlpha())
|
||||
, ("amount", reagent.Quantity)));
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private void OnExamineSolution(EntityUid uid, ExaminableSolutionComponent examinableComponent,
|
||||
ExaminedEvent args)
|
||||
{
|
||||
|
||||
@@ -20,18 +20,20 @@ public sealed class SolutionRandomFillSystem : EntitySystem
|
||||
SubscribeLocalEvent<RandomFillSolutionComponent, MapInitEvent>(OnRandomSolutionFillMapInit);
|
||||
}
|
||||
|
||||
public void OnRandomSolutionFillMapInit(EntityUid uid, RandomFillSolutionComponent component, MapInitEvent args)
|
||||
private void OnRandomSolutionFillMapInit(EntityUid uid, RandomFillSolutionComponent component, MapInitEvent args)
|
||||
{
|
||||
var target = _solutionsSystem.EnsureSolution(uid, component.Solution);
|
||||
var reagent = _proto.Index<WeightedRandomPrototype>(component.WeightedRandomId).Pick(_random);
|
||||
var pick = _proto.Index<WeightedRandomFillSolutionPrototype>(component.WeightedRandomId).Pick(_random);
|
||||
|
||||
if (!_proto.TryIndex<ReagentPrototype>(reagent, out ReagentPrototype? reagentProto))
|
||||
var reagent = pick.reagent;
|
||||
var quantity = pick.quantity;
|
||||
|
||||
if (!_proto.HasIndex<ReagentPrototype>(reagent))
|
||||
{
|
||||
Logger.Error(
|
||||
$"Tried to add invalid reagent Id {reagent} using SolutionRandomFill.");
|
||||
Log.Error($"Tried to add invalid reagent Id {reagent} using SolutionRandomFill.");
|
||||
return;
|
||||
}
|
||||
|
||||
target.AddReagent(reagent, component.Quantity);
|
||||
target.AddReagent(reagent, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Localizations;
|
||||
using JetBrains.Annotations;
|
||||
@@ -28,22 +29,65 @@ namespace Content.Server.Chemistry.ReagentEffects
|
||||
/// </summary>
|
||||
[JsonPropertyName("scaleByQuantity")]
|
||||
[DataField("scaleByQuantity")]
|
||||
public bool ScaleByQuantity = false;
|
||||
public bool ScaleByQuantity;
|
||||
|
||||
[DataField("ignoreResistances")]
|
||||
[JsonPropertyName("ignoreResistances")]
|
||||
public bool IgnoreResistances = true;
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
{
|
||||
var damages = new List<string>();
|
||||
var heals = false;
|
||||
var deals = false;
|
||||
|
||||
// TODO: This should be smarter. Namely, not showing a damage type as being in a group unless every damage type in the group is present and equal in value.
|
||||
foreach (var (kind, amount) in Damage.GetDamagePerGroup())
|
||||
var damageSpec = new DamageSpecifier(Damage);
|
||||
|
||||
foreach (var group in prototype.EnumeratePrototypes<DamageGroupPrototype>())
|
||||
{
|
||||
var sign = MathF.Sign(amount.Float());
|
||||
if (!damageSpec.TryGetDamageInGroup(group, out var amount))
|
||||
continue;
|
||||
|
||||
var relevantTypes = damageSpec.DamageDict
|
||||
.Where(x => x.Value != FixedPoint2.Zero && group.DamageTypes.Contains(x.Key)).ToList();
|
||||
|
||||
if (relevantTypes.Count != group.DamageTypes.Count)
|
||||
continue;
|
||||
|
||||
var sum = FixedPoint2.Zero;
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
sum += damageSpec.DamageDict.GetValueOrDefault(type);
|
||||
}
|
||||
|
||||
// if the total sum of all the types equal the damage amount,
|
||||
// assume that they're evenly distributed.
|
||||
if (sum != amount)
|
||||
continue;
|
||||
|
||||
var sign = FixedPoint2.Sign(amount);
|
||||
|
||||
if (sign < 0)
|
||||
heals = true;
|
||||
if (sign > 0)
|
||||
deals = true;
|
||||
|
||||
damages.Add(
|
||||
Loc.GetString("health-change-display",
|
||||
("kind", group.ID),
|
||||
("amount", MathF.Abs(amount.Float())),
|
||||
("deltasign", sign)
|
||||
));
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
damageSpec.DamageDict.Remove(type);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (kind, amount) in damageSpec.DamageDict)
|
||||
{
|
||||
var sign = FixedPoint2.Sign(amount);
|
||||
|
||||
if (sign < 0)
|
||||
heals = true;
|
||||
@@ -71,7 +115,7 @@ namespace Content.Server.Chemistry.ReagentEffects
|
||||
var scale = ScaleByQuantity ? args.Quantity : FixedPoint2.New(1);
|
||||
scale *= args.Scale;
|
||||
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(args.SolutionEntity, Damage * scale, IgnoreResistances);
|
||||
args.EntityManager.System<DamageableSystem>().TryChangeDamage(args.SolutionEntity, Damage * scale, IgnoreResistances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Stacks;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Construction;
|
||||
|
||||
@@ -48,115 +49,189 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, MachineFrameComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (!component.HasBoard && TryComp<MachineBoardComponent?>(args.Used, out var machineBoard))
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!component.HasBoard)
|
||||
{
|
||||
if (_container.TryRemoveFromContainer(args.Used))
|
||||
{
|
||||
// Valid board!
|
||||
component.BoardContainer.Insert(args.Used);
|
||||
|
||||
// Setup requirements and progress...
|
||||
ResetProgressAndRequirements(component, machineBoard);
|
||||
|
||||
if (TryComp(uid, out ConstructionComponent? construction))
|
||||
{
|
||||
// So prying the components off works correctly.
|
||||
_construction.ResetEdge(uid, construction);
|
||||
}
|
||||
}
|
||||
if (TryInsertBoard(uid, args.Used, component))
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
else if (component.HasBoard)
|
||||
|
||||
// Machine parts cannot currently satisfy stack/component/tag restrictions. Similarly stacks cannot satisfy
|
||||
// component/tag restrictions. However, there is no reason this cannot be supported in the future. If this
|
||||
// changes, then RegenerateProgress() also needs to be updated.
|
||||
//
|
||||
// Note that one entity is ALLOWED to satisfy more than one kind of component or tag requirements. This is
|
||||
// necessary in order to avoid weird entity-ordering shenanigans in RegenerateProgress().
|
||||
|
||||
// Handle parts
|
||||
if (TryComp<MachinePartComponent>(args.Used, out var machinePart))
|
||||
{
|
||||
if (TryComp<MachinePartComponent>(args.Used, out var machinePart))
|
||||
{
|
||||
if (!component.Requirements.ContainsKey(machinePart.PartType))
|
||||
return;
|
||||
|
||||
if (component.Progress[machinePart.PartType] != component.Requirements[machinePart.PartType]
|
||||
&& _container.TryRemoveFromContainer(args.Used) && component.PartContainer.Insert(args.Used))
|
||||
{
|
||||
component.Progress[machinePart.PartType]++;
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.Handled && TryComp<StackComponent?>(args.Used, out var stack))
|
||||
{
|
||||
var type = stack.StackTypeId;
|
||||
if (type == null)
|
||||
return;
|
||||
if (!component.MaterialRequirements.ContainsKey(type))
|
||||
return;
|
||||
|
||||
if (component.MaterialProgress[type] == component.MaterialRequirements[type])
|
||||
return;
|
||||
|
||||
var needed = component.MaterialRequirements[type] - component.MaterialProgress[type];
|
||||
var count = stack.Count;
|
||||
|
||||
if (count < needed)
|
||||
{
|
||||
if (!component.PartContainer.Insert(stack.Owner))
|
||||
return;
|
||||
|
||||
component.MaterialProgress[type] += count;
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var splitStack = _stack.Split(args.Used, needed,
|
||||
Comp<TransformComponent>(uid).Coordinates, stack);
|
||||
|
||||
if (splitStack == null)
|
||||
return;
|
||||
|
||||
if (!component.PartContainer.Insert(splitStack.Value))
|
||||
return;
|
||||
|
||||
component.MaterialProgress[type] += needed;
|
||||
if (TryInsertPart(uid, args.Used, component, machinePart))
|
||||
args.Handled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Handled)
|
||||
{
|
||||
if (IsComplete(component)) {
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (compName, info) in component.ComponentRequirements)
|
||||
{
|
||||
if (component.ComponentProgress[compName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
var registration = _factory.GetRegistration(compName);
|
||||
|
||||
if (!HasComp(args.Used, registration.Type))
|
||||
continue;
|
||||
|
||||
if (!_container.TryRemoveFromContainer(args.Used) || !component.PartContainer.Insert(args.Used))
|
||||
continue;
|
||||
component.ComponentProgress[compName]++;
|
||||
// Handle stacks
|
||||
if (TryComp<StackComponent?>(args.Used, out var stack))
|
||||
{
|
||||
if (TryInsertStack(uid, args.Used, component, stack))
|
||||
args.Handled = true;
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle component requirements
|
||||
foreach (var (compName, info) in component.ComponentRequirements)
|
||||
{
|
||||
if (component.ComponentProgress[compName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
var registration = _factory.GetRegistration(compName);
|
||||
|
||||
if (!HasComp(args.Used, registration.Type))
|
||||
continue;
|
||||
|
||||
// Insert the entity, if it hasn't already been inserted
|
||||
if (!args.Handled)
|
||||
{
|
||||
if (!_container.TryRemoveFromContainer(args.Used))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
if (!component.PartContainer.Insert(args.Used))
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (tagName, info) in component.TagRequirements)
|
||||
component.ComponentProgress[compName]++;
|
||||
|
||||
if (IsComplete(component))
|
||||
{
|
||||
if (component.TagProgress[tagName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
if (!_tag.HasTag(args.Used, tagName))
|
||||
continue;
|
||||
|
||||
if (!_container.TryRemoveFromContainer(args.Used) || !component.PartContainer.Insert(args.Used))
|
||||
continue;
|
||||
component.TagProgress[tagName]++;
|
||||
args.Handled = true;
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tag requirements
|
||||
if (!TryComp<TagComponent>(args.Used, out var tagComp))
|
||||
return;
|
||||
|
||||
foreach (var (tagName, info) in component.TagRequirements)
|
||||
{
|
||||
if (component.TagProgress[tagName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
if (!_tag.HasTag(tagComp, tagName))
|
||||
continue;
|
||||
|
||||
// Insert the entity, if it hasn't already been inserted
|
||||
if (!args.Handled)
|
||||
{
|
||||
if (!_container.TryRemoveFromContainer(args.Used))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
if (!component.PartContainer.Insert(args.Used))
|
||||
return;
|
||||
}
|
||||
|
||||
component.TagProgress[tagName]++;
|
||||
args.Handled = true;
|
||||
|
||||
if (IsComplete(component))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>Whether or not the function had any effect. Does not indicate success.</returns>
|
||||
private bool TryInsertBoard(EntityUid uid, EntityUid used, MachineFrameComponent component)
|
||||
{
|
||||
if (!TryComp<MachineBoardComponent?>(used, out var machineBoard))
|
||||
return false;
|
||||
|
||||
if (!_container.TryRemoveFromContainer(used))
|
||||
return false;
|
||||
|
||||
if (!component.BoardContainer.Insert(used))
|
||||
return true;
|
||||
|
||||
ResetProgressAndRequirements(component, machineBoard);
|
||||
|
||||
// Reset edge so that prying the components off works correctly.
|
||||
if (TryComp(uid, out ConstructionComponent? construction))
|
||||
_construction.ResetEdge(uid, construction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <returns>Whether or not the function had any effect. Does not indicate success.</returns>
|
||||
private bool TryInsertPart(EntityUid uid, EntityUid used, MachineFrameComponent component, MachinePartComponent machinePart)
|
||||
{
|
||||
DebugTools.Assert(!HasComp<StackComponent>(uid));
|
||||
if (!component.Requirements.ContainsKey(machinePart.PartType))
|
||||
return false;
|
||||
|
||||
if (component.Progress[machinePart.PartType] >= component.Requirements[machinePart.PartType])
|
||||
return false;
|
||||
|
||||
if (!_container.TryRemoveFromContainer(used))
|
||||
return false;
|
||||
|
||||
if (!component.PartContainer.Insert(used))
|
||||
return true;
|
||||
|
||||
component.Progress[machinePart.PartType]++;
|
||||
if (IsComplete(component))
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <returns>Whether or not the function had any effect. Does not indicate success.</returns>
|
||||
private bool TryInsertStack(EntityUid uid, EntityUid used, MachineFrameComponent component, StackComponent stack)
|
||||
{
|
||||
var type = stack.StackTypeId;
|
||||
|
||||
if (!component.MaterialRequirements.ContainsKey(type))
|
||||
return false;
|
||||
|
||||
var progress = component.MaterialProgress[type];
|
||||
var requirement = component.MaterialRequirements[type];
|
||||
var needed = requirement - progress;
|
||||
|
||||
if (needed <= 0)
|
||||
return false;
|
||||
|
||||
var count = stack.Count;
|
||||
if (count < needed)
|
||||
{
|
||||
if (!_container.TryRemoveFromContainer(used))
|
||||
return false;
|
||||
|
||||
if (!component.PartContainer.Insert(used))
|
||||
return true;
|
||||
|
||||
component.MaterialProgress[type] += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack);
|
||||
|
||||
if (splitStack == null)
|
||||
return false;
|
||||
|
||||
if (!component.PartContainer.Insert(splitStack.Value))
|
||||
return true;
|
||||
|
||||
component.MaterialProgress[type] += needed;
|
||||
if (IsComplete(component))
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsComplete(MachineFrameComponent component)
|
||||
@@ -247,10 +322,14 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
|
||||
ResetProgressAndRequirements(component, machineBoard);
|
||||
|
||||
// If the following code is updated, you need to make sure that it matches the logic in OnInteractUsing()
|
||||
|
||||
foreach (var part in component.PartContainer.ContainedEntities)
|
||||
{
|
||||
if (TryComp<MachinePartComponent>(part, out var machinePart))
|
||||
{
|
||||
DebugTools.Assert(!HasComp<StackComponent>(part));
|
||||
|
||||
// Check this is part of the requirements...
|
||||
if (!component.Requirements.ContainsKey(machinePart.PartType))
|
||||
continue;
|
||||
@@ -259,21 +338,23 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
component.Progress[machinePart.PartType] = 1;
|
||||
else
|
||||
component.Progress[machinePart.PartType]++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp<StackComponent>(part, out var stack))
|
||||
{
|
||||
var type = stack.StackTypeId;
|
||||
// Check this is part of the requirements...
|
||||
if (type == null)
|
||||
continue;
|
||||
|
||||
if (!component.MaterialRequirements.ContainsKey(type))
|
||||
continue;
|
||||
|
||||
if (!component.MaterialProgress.ContainsKey(type))
|
||||
component.MaterialProgress[type] = 1;
|
||||
component.MaterialProgress[type] = stack.Count;
|
||||
else
|
||||
component.MaterialProgress[type]++;
|
||||
component.MaterialProgress[type] += stack.Count;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// I have many regrets.
|
||||
@@ -290,10 +371,13 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
component.ComponentProgress[compName]++;
|
||||
}
|
||||
|
||||
if (!TryComp<TagComponent>(part, out var tagComp))
|
||||
continue;
|
||||
|
||||
// I have MANY regrets.
|
||||
foreach (var (tagName, _) in component.TagRequirements)
|
||||
foreach (var tagName in component.TagRequirements.Keys)
|
||||
{
|
||||
if (!_tag.HasTag(part, tagName))
|
||||
if (!_tag.HasTag(tagComp, tagName))
|
||||
continue;
|
||||
|
||||
if (!component.TagProgress.ContainsKey(tagName))
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
using Content.Server.Damage.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Damage.Components
|
||||
namespace Content.Server.Damage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Should the entity take damage / be stunned if colliding at a speed above MinimumSpeed?
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(DamageOnHighSpeedImpactSystem))]
|
||||
public sealed class DamageOnHighSpeedImpactComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Should the entity take damage / be stunned if colliding at a speed above MinimumSpeed?
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
internal sealed class DamageOnHighSpeedImpactComponent : Component
|
||||
{
|
||||
[DataField("minimumSpeed")]
|
||||
public float MinimumSpeed { get; set; } = 20f;
|
||||
[DataField("factor")]
|
||||
public float Factor { get; set; } = 0.5f;
|
||||
[DataField("soundHit", required: true)]
|
||||
public SoundSpecifier SoundHit { get; set; } = default!;
|
||||
[DataField("stunChance")]
|
||||
public float StunChance { get; set; } = 0.25f;
|
||||
[DataField("stunMinimumDamage")]
|
||||
public int StunMinimumDamage { get; set; } = 10;
|
||||
[DataField("stunSeconds")]
|
||||
public float StunSeconds { get; set; } = 1f;
|
||||
[DataField("damageCooldown")]
|
||||
public float DamageCooldown { get; set; } = 2f;
|
||||
[DataField("minimumSpeed"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MinimumSpeed = 20f;
|
||||
|
||||
internal TimeSpan LastHit = TimeSpan.Zero;
|
||||
[DataField("speedDamageFactor"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpeedDamageFactor = 0.5f;
|
||||
|
||||
[DataField("damage", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier Damage = default!;
|
||||
}
|
||||
[DataField("soundHit", required: true), ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier SoundHit = default!;
|
||||
|
||||
[DataField("stunChance"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float StunChance = 0.25f;
|
||||
|
||||
[DataField("stunMinimumDamage"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int StunMinimumDamage = 10;
|
||||
|
||||
[DataField("stunSeconds"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float StunSeconds = 1f;
|
||||
|
||||
[DataField("damageCooldown"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DamageCooldown = 2f;
|
||||
|
||||
[DataField("lastHit", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan LastHit = TimeSpan.Zero;
|
||||
|
||||
[DataField("damage", required: true), ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier Damage = default!;
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Damage;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Shared.Effects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Damage.Systems
|
||||
namespace Content.Server.Damage.Systems;
|
||||
|
||||
public sealed class DamageOnHighSpeedImpactSystem : EntitySystem
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class DamageOnHighSpeedImpactSystem: EntitySystem
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly StunSystem _stunSystem = default!;
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DamageOnHighSpeedImpactComponent, StartCollideEvent>(HandleCollide);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DamageOnHighSpeedImpactComponent, StartCollideEvent>(HandleCollide);
|
||||
}
|
||||
private void HandleCollide(EntityUid uid, DamageOnHighSpeedImpactComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (!args.OurFixture.Hard || !args.OtherFixture.Hard)
|
||||
return;
|
||||
|
||||
private void HandleCollide(EntityUid uid, DamageOnHighSpeedImpactComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (!EntityManager.HasComponent<DamageableComponent>(uid))
|
||||
return;
|
||||
if (!EntityManager.HasComponent<DamageableComponent>(uid))
|
||||
return;
|
||||
|
||||
var otherBody = args.OtherEntity;
|
||||
var speed = args.OurBody.LinearVelocity.Length();
|
||||
var speed = args.OurBody.LinearVelocity.Length();
|
||||
|
||||
if (speed < component.MinimumSpeed)
|
||||
return;
|
||||
if (speed < component.MinimumSpeed)
|
||||
return;
|
||||
|
||||
SoundSystem.Play(component.SoundHit.GetSound(), Filter.Pvs(otherBody), otherBody, AudioHelpers.WithVariation(0.125f).WithVolume(-0.125f));
|
||||
if ((_gameTiming.CurTime - component.LastHit).TotalSeconds < component.DamageCooldown)
|
||||
return;
|
||||
|
||||
if ((_gameTiming.CurTime - component.LastHit).TotalSeconds < component.DamageCooldown)
|
||||
return;
|
||||
component.LastHit = _gameTiming.CurTime;
|
||||
|
||||
component.LastHit = _gameTiming.CurTime;
|
||||
if (_robustRandom.Prob(component.StunChance))
|
||||
_stun.TryStun(uid, TimeSpan.FromSeconds(component.StunSeconds), true);
|
||||
|
||||
if (_robustRandom.Prob(component.StunChance))
|
||||
_stunSystem.TryStun(uid, TimeSpan.FromSeconds(component.StunSeconds), true);
|
||||
var damageScale = component.SpeedDamageFactor * speed / component.MinimumSpeed;
|
||||
|
||||
var damageScale = (speed / component.MinimumSpeed) * component.Factor;
|
||||
_damageable.TryChangeDamage(uid, component.Damage * damageScale);
|
||||
|
||||
_damageableSystem.TryChangeDamage(uid, component.Damage * damageScale);
|
||||
}
|
||||
_audio.PlayPvs(component.SoundHit, uid, AudioParams.Default.WithVariation(0.125f).WithVolume(-0.125f));
|
||||
RaiseNetworkEvent(new ColorFlashEffectEvent(Color.Red, new List<EntityUid> { uid }), Filter.Pvs(uid, entityManager: EntityManager));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Effects;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Throwing;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Damage.Systems
|
||||
{
|
||||
public sealed class DamageOtherOnHitSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly GunSystem _guns = default!;
|
||||
[Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!;
|
||||
[Dependency] private readonly ThrownItemSystem _thrownItem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -19,11 +27,21 @@ namespace Content.Server.Damage.Systems
|
||||
|
||||
private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args)
|
||||
{
|
||||
var dmg = _damageableSystem.TryChangeDamage(args.Target, component.Damage, component.IgnoreResistances, origin: args.User);
|
||||
var dmg = _damageableSystem.TryChangeDamage(args.Target, component.Damage, component.IgnoreResistances, origin: args.Component.Thrower);
|
||||
|
||||
// Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying.
|
||||
if (dmg != null && HasComp<MobStateComponent>(args.Target))
|
||||
_adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {dmg.Total:damage} damage from collision");
|
||||
|
||||
RaiseNetworkEvent(new ColorFlashEffectEvent(Color.Red, new List<EntityUid> { args.Target }), Filter.Pvs(args.Target, entityManager: EntityManager));
|
||||
_guns.PlayImpactSound(args.Target, dmg, null, false);
|
||||
if (TryComp<PhysicsComponent>(uid, out var body) && body.LinearVelocity.LengthSquared() > 0f)
|
||||
{
|
||||
var direction = body.LinearVelocity.Normalized();
|
||||
_sharedCameraRecoil.KickCamera(args.Target, direction);
|
||||
}
|
||||
|
||||
_thrownItem.LandComponent(args.Thrown, args.Component, playSound: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,19 @@ public sealed class DeviceListSystem : SharedDeviceListSystem
|
||||
return devices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given address is present in a device list
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity uid that has the device list that should be checked for the address</param>
|
||||
/// <param name="address">The address to check for</param>
|
||||
/// <param name="deviceList">The device list component</param>
|
||||
/// <returns>True if the address is present. False if not</returns>
|
||||
public bool ExistsInDeviceList(EntityUid uid, string address, DeviceListComponent? deviceList = null)
|
||||
{
|
||||
var addresses = GetDeviceList(uid).Keys;
|
||||
return addresses.Contains(address);
|
||||
}
|
||||
|
||||
protected override void UpdateShutdownSubscription(EntityUid uid, List<EntityUid> newDevices, List<EntityUid> oldDevices)
|
||||
{
|
||||
foreach (var device in newDevices)
|
||||
|
||||
@@ -309,8 +309,8 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.User != null)
|
||||
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.User.Value):player} landed in {ToPrettyString(uid)}");
|
||||
if (args.Component.Thrower != null)
|
||||
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.Component.Thrower.Value):player} landed in {ToPrettyString(uid)}");
|
||||
|
||||
AfterInsert(uid, component, args.Thrown);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Sprite;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Content.Server.Dragon;
|
||||
|
||||
@@ -29,6 +31,7 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ISerializationManager _serManager = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
@@ -149,8 +152,18 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
if (comp.SpawnAccumulator > comp.SpawnCooldown)
|
||||
{
|
||||
comp.SpawnAccumulator -= comp.SpawnCooldown;
|
||||
var ent = Spawn(comp.SpawnPrototype, Transform(comp.Owner).MapPosition);
|
||||
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Owner, Vector2.Zero));
|
||||
var ent = Spawn(comp.SpawnPrototype, Transform(comp.Owner).Coordinates);
|
||||
|
||||
// Update their look to match the leader.
|
||||
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
|
||||
{
|
||||
var spawnedSprite = EnsureComp<RandomSpriteComponent>(ent);
|
||||
_serManager.CopyTo(randomSprite, ref spawnedSprite, notNullableOverride: true);
|
||||
Dirty(ent, spawnedSprite);
|
||||
}
|
||||
|
||||
if (comp.Dragon != null)
|
||||
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Dragon.Value, Vector2.Zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a gun when attempting to shoot while it's empty
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class TriggerWhenEmptyComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -22,7 +22,7 @@ using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems
|
||||
{
|
||||
@@ -77,6 +77,7 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
|
||||
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredEvent>(OnStepTriggered);
|
||||
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
||||
SubscribeLocalEvent<TriggerWhenEmptyComponent, OnEmptyGunShotEvent>(OnEmptyTriggered);
|
||||
|
||||
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(OnSpawnTrigger);
|
||||
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
|
||||
@@ -186,6 +187,11 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
Trigger(uid, args.Slipped);
|
||||
}
|
||||
|
||||
private void OnEmptyTriggered(EntityUid uid, TriggerWhenEmptyComponent component, ref OnEmptyGunShotEvent args)
|
||||
{
|
||||
Trigger(uid, args.EmptyGun);
|
||||
}
|
||||
|
||||
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
||||
{
|
||||
var triggerEvent = new TriggerEvent(trigger, user);
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Friction;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.StepTrigger.Components;
|
||||
using Content.Shared.StepTrigger.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -511,7 +512,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
}
|
||||
|
||||
_reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
|
||||
_popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
|
||||
_popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
|
||||
}
|
||||
|
||||
return TrySpillAt(coordinates, solution, out puddleUid, sound);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
@@ -17,8 +18,16 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
public const float PresetFailedCooldownIncrease = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// The selected preset that will be used at the start of the next round.
|
||||
/// </summary>
|
||||
public GamePresetPrototype? Preset { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preset that's currently active.
|
||||
/// </summary>
|
||||
public GamePresetPrototype? CurrentPreset { get; private set; }
|
||||
|
||||
private bool StartPreset(IPlayerSession[] origReadyPlayers, bool force)
|
||||
{
|
||||
var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
|
||||
@@ -27,7 +36,7 @@ namespace Content.Server.GameTicking
|
||||
if (!startAttempt.Cancelled)
|
||||
return true;
|
||||
|
||||
var presetTitle = Preset != null ? Loc.GetString(Preset.ModeTitle) : string.Empty;
|
||||
var presetTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
|
||||
|
||||
void FailedPresetRestart()
|
||||
{
|
||||
@@ -93,6 +102,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
Preset = preset;
|
||||
UpdateInfoText();
|
||||
ValidateMap();
|
||||
|
||||
if (force)
|
||||
{
|
||||
@@ -131,12 +141,39 @@ namespace Content.Server.GameTicking
|
||||
return prototype != null;
|
||||
}
|
||||
|
||||
public bool IsMapEligible(GameMapPrototype map)
|
||||
{
|
||||
if (Preset == null)
|
||||
return true;
|
||||
|
||||
if (Preset.MapPool == null || !_prototypeManager.TryIndex<GameMapPoolPrototype>(Preset.MapPool, out var pool))
|
||||
return true;
|
||||
|
||||
return pool.Maps.Contains(map.ID);
|
||||
}
|
||||
|
||||
private void ValidateMap()
|
||||
{
|
||||
if (Preset == null || _gameMapManager.GetSelectedMap() is not { } map)
|
||||
return;
|
||||
|
||||
if (Preset.MapPool == null ||
|
||||
!_prototypeManager.TryIndex<GameMapPoolPrototype>(Preset.MapPool, out var pool))
|
||||
return;
|
||||
|
||||
if (pool.Maps.Contains(map.ID))
|
||||
return;
|
||||
|
||||
_gameMapManager.SelectMapRandom();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
private bool AddGamePresetRules()
|
||||
{
|
||||
if (DummyTicker || Preset == null)
|
||||
return false;
|
||||
|
||||
CurrentPreset = Preset;
|
||||
foreach (var rule in Preset.Rules)
|
||||
{
|
||||
AddGameRule(rule);
|
||||
|
||||
@@ -44,7 +44,8 @@ namespace Content.Server.GameTicking
|
||||
|
||||
private string GetInfoText()
|
||||
{
|
||||
if (Preset == null)
|
||||
var preset = CurrentPreset ?? Preset;
|
||||
if (preset == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -72,8 +73,8 @@ namespace Content.Server.GameTicking
|
||||
stationNames.Append(Loc.GetString("game-ticker-no-map-selected"));
|
||||
}
|
||||
|
||||
var gmTitle = Loc.GetString(Preset.ModeTitle);
|
||||
var desc = Loc.GetString(Preset.Description);
|
||||
var gmTitle = Loc.GetString(preset.ModeTitle);
|
||||
var desc = Loc.GetString(preset.Description);
|
||||
return Loc.GetString(RunLevel == GameRunLevel.PreRoundLobby ? "game-ticker-get-info-preround-text" : "game-ticker-get-info-text",
|
||||
("roundId", RoundId), ("playerCount", playerCount), ("readyCount", readyCount), ("mapName", stationNames.ToString()),("gmTitle", gmTitle),("desc", desc));
|
||||
}
|
||||
|
||||
@@ -114,6 +114,17 @@ namespace Content.Server.GameTicking
|
||||
throw new Exception("invalid config; couldn't select a valid station map!");
|
||||
}
|
||||
|
||||
if (CurrentPreset?.MapPool != null &&
|
||||
_prototypeManager.TryIndex<GameMapPoolPrototype>(CurrentPreset.MapPool, out var pool) &&
|
||||
pool.Maps.Contains(mainStationMap.ID))
|
||||
{
|
||||
var msg = Loc.GetString("game-ticker-start-round-invalid-map",
|
||||
("map", mainStationMap.MapName),
|
||||
("mode", Loc.GetString(CurrentPreset.ModeTitle)));
|
||||
Log.Debug(msg);
|
||||
SendServerMessage(msg);
|
||||
}
|
||||
|
||||
// Let game rules dictate what maps we should load.
|
||||
RaiseLocalEvent(new LoadingMapsEvent(maps));
|
||||
|
||||
@@ -293,7 +304,7 @@ namespace Content.Server.GameTicking
|
||||
_adminLogger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Round ended, showing summary");
|
||||
|
||||
//Tell every client the round has ended.
|
||||
var gamemodeTitle = Preset != null ? Loc.GetString(Preset.ModeTitle) : string.Empty;
|
||||
var gamemodeTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
|
||||
|
||||
// Let things add text here.
|
||||
var textEv = new RoundEndTextAppendEvent();
|
||||
@@ -307,7 +318,7 @@ namespace Content.Server.GameTicking
|
||||
//Generate a list of basic player info to display in the end round summary.
|
||||
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
|
||||
// Grab the great big book of all the Minds, we'll need them for this.
|
||||
var allMinds = Get<MindTrackerSystem>().AllMinds;
|
||||
var allMinds = _mindTracker.AllMinds;
|
||||
foreach (var mind in allMinds)
|
||||
{
|
||||
// TODO don't list redundant observer roles?
|
||||
@@ -344,7 +355,7 @@ namespace Content.Server.GameTicking
|
||||
PlayerOOCName = contentPlayerData?.Name ?? "(IMPOSSIBLE: REGISTERED MIND WITH NO OWNER)",
|
||||
// Character name takes precedence over current entity name
|
||||
PlayerICName = playerIcName,
|
||||
PlayerEntityUid = mind.OwnedEntity,
|
||||
PlayerEntityUid = mind.OriginalOwnedEntity,
|
||||
Role = antag
|
||||
? mind.AllRoles.First(role => role.Antagonist).Name
|
||||
: mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("game-ticker-unknown-role"),
|
||||
@@ -449,6 +460,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
// Clear up any game rules.
|
||||
ClearGameRules();
|
||||
CurrentPreset = null;
|
||||
|
||||
_allPreviousGameRules.Clear();
|
||||
|
||||
@@ -516,7 +528,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
private void AnnounceRound()
|
||||
{
|
||||
if (Preset == null) return;
|
||||
if (CurrentPreset == null) return;
|
||||
|
||||
var options = _prototypeManager.EnumeratePrototypes<RoundAnnouncementPrototype>().ToList();
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Preferences;
|
||||
@@ -22,6 +24,8 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
public sealed partial class GameTicker
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
private const string ObserverPrototypeName = "MobObserver";
|
||||
|
||||
/// <summary>
|
||||
@@ -133,6 +137,10 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatically de-admin players who are joining.
|
||||
if (_cfg.GetCVar(CCVars.AdminDeadminOnJoin) && _adminManager.IsAdmin(player))
|
||||
_adminManager.DeAdmin(player);
|
||||
|
||||
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
|
||||
var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
|
||||
RaiseLocalEvent(bev);
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Content.Server.GameTicking
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MindTrackerSystem _mindTracker = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
@@ -92,7 +93,8 @@ namespace Content.Server.GameTicking
|
||||
|
||||
private void SendServerMessage(string message)
|
||||
{
|
||||
_chatManager.ChatMessageToAll(ChatChannel.Server, message, "", default, false, true);
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
|
||||
_chatManager.ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, default, false, true);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
using Content.Server.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Server.GameTicking.Presets
|
||||
@@ -33,5 +35,12 @@ namespace Content.Server.GameTicking.Presets
|
||||
|
||||
[DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||
public IReadOnlyList<string> Rules { get; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// If specified, the gamemode will only be run with these maps.
|
||||
/// If none are elligible, the global fallback will be used.
|
||||
/// </summary>
|
||||
[DataField("supportedMaps", customTypeSerializer: typeof(PrototypeIdSerializer<GameMapPoolPrototype>))]
|
||||
public readonly string? MapPool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,12 +11,12 @@ namespace Content.Server.GameTicking.Rules.Components;
|
||||
[RegisterComponent]
|
||||
public sealed class NukeOperativeSpawnerComponent : Component
|
||||
{
|
||||
[DataField("name")]
|
||||
public string OperativeName = "";
|
||||
[DataField("name", required:true)]
|
||||
public string OperativeName = default!;
|
||||
|
||||
[DataField("rolePrototype")]
|
||||
public string OperativeRolePrototype = "";
|
||||
[DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<AntagPrototype>), required:true)]
|
||||
public string OperativeRolePrototype = default!;
|
||||
|
||||
[DataField("startingGearPrototype")]
|
||||
public string OperativeStartingGear = "";
|
||||
[DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<StartingGearPrototype>), required:true)]
|
||||
public string OperativeStartingGear = default!;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.Roles;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
@@ -43,19 +44,19 @@ public sealed class NukeopsRuleComponent : Component
|
||||
[DataField("spawnOutpost")]
|
||||
public bool SpawnOutpost = true;
|
||||
|
||||
[DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
[DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string SpawnPointPrototype = "SpawnPointNukies";
|
||||
|
||||
[DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
[DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
|
||||
|
||||
[DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
[DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
|
||||
public string CommanderRolePrototype = "NukeopsCommander";
|
||||
|
||||
[DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
[DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
|
||||
public string OperativeRoleProto = "Nukeops";
|
||||
|
||||
[DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
[DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
|
||||
public string MedicRoleProto = "NukeopsMedic";
|
||||
|
||||
[DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
|
||||
@@ -465,7 +465,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
}
|
||||
}
|
||||
|
||||
var numNukies = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives);
|
||||
var numNukies = MathHelper.Clamp(_playerSystem.PlayerCount / playersPerOperative, 1, maxOperatives);
|
||||
|
||||
for (var i = 0; i < numNukies; i++)
|
||||
{
|
||||
@@ -580,6 +580,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
// todo: this is kinda awful for multi-nukies
|
||||
foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
|
||||
{
|
||||
if (nukeOpSpawner.OperativeName == null
|
||||
|| nukeOpSpawner.OperativeStartingGear == null
|
||||
|| nukeOpSpawner.OperativeRolePrototype == null)
|
||||
{
|
||||
// I have no idea what is going on with nuke ops code, but I'm pretty sure this shouldn't be possible.
|
||||
Log.Error($"Invalid nuke op spawner: {ToPrettyString(spawner)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile, nukeops);
|
||||
|
||||
nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);
|
||||
|
||||
40
Content.Server/Gateway/Components/GatewayComponent.cs
Normal file
40
Content.Server/Gateway/Components/GatewayComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Server.Gateway.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Gateway.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Controlling gateway that links to other gateway destinations on the server.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(GatewaySystem))]
|
||||
public sealed class GatewayComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Sound to play when opening or closing the portal.
|
||||
/// </summary>
|
||||
[DataField("portalSound")]
|
||||
public SoundSpecifier PortalSound = new SoundPathSpecifier("/Audio/Effects/Lightning/lightningbolt.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Every other gateway destination on the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Added on
|
||||
/// </remarks>
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> Destinations = new();
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the portal will be closed.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("nextClose", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextClose;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the portal was last opened.
|
||||
/// Only used for UI.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan LastOpen;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Server.Gateway.Systems;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Gateway.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A gateway destination linked to by station gateway(s).
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(GatewaySystem))]
|
||||
public sealed class GatewayDestinationComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this destination is shown in the gateway ui.
|
||||
/// If you are making a gateway for an admeme set this once you are ready for players to select it.
|
||||
/// </summary>
|
||||
[DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Name as it shows up on the ui of station gateways.
|
||||
/// </summary>
|
||||
[DataField("name"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Time at which this destination is ready to be linked to.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("nextReady", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextReady;
|
||||
|
||||
/// <summary>
|
||||
/// How long the portal will be open for after linking.
|
||||
/// </summary>
|
||||
[DataField("openTime"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan OpenTime = TimeSpan.FromSeconds(600);
|
||||
|
||||
/// <summary>
|
||||
/// How long the destination is not ready for after the portal closes.
|
||||
/// </summary>
|
||||
[DataField("cooldown"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan Cooldown = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
181
Content.Server/Gateway/Systems/GatewaySystem.cs
Normal file
181
Content.Server/Gateway/Systems/GatewaySystem.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using Content.Server.Gateway.Components;
|
||||
using Content.Shared.Gateway;
|
||||
using Content.Shared.Teleportation.Components;
|
||||
using Content.Shared.Teleportation.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Gateway.Systems;
|
||||
|
||||
public sealed class GatewaySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly LinkedEntitySystem _linkedEntity = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GatewayComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<GatewayComponent, BoundUIOpenedEvent>(UpdateUserInterface);
|
||||
SubscribeLocalEvent<GatewayComponent, GatewayOpenPortalMessage>(OnOpenPortal);
|
||||
|
||||
SubscribeLocalEvent<GatewayDestinationComponent, ComponentStartup>(OnDestinationStartup);
|
||||
SubscribeLocalEvent<GatewayDestinationComponent, ComponentShutdown>(OnDestinationShutdown);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// close portals after theyve been open long enough
|
||||
var query = EntityQueryEnumerator<GatewayComponent, PortalComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp, out var _))
|
||||
{
|
||||
if (_timing.CurTime < comp.NextClose)
|
||||
continue;
|
||||
|
||||
ClosePortal(uid, comp);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, GatewayComponent comp, ComponentStartup args)
|
||||
{
|
||||
// add existing destinations
|
||||
var query = EntityQueryEnumerator<GatewayDestinationComponent>();
|
||||
while (query.MoveNext(out var dest, out _))
|
||||
{
|
||||
comp.Destinations.Add(dest);
|
||||
}
|
||||
|
||||
// no need to update ui since its just been created, just do portal
|
||||
UpdateAppearance(uid);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface<T>(EntityUid uid, GatewayComponent comp, T args)
|
||||
{
|
||||
UpdateUserInterface(uid, comp);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(EntityUid uid, GatewayComponent comp)
|
||||
{
|
||||
var destinations = new List<(EntityUid, String, TimeSpan, bool)>();
|
||||
foreach (var destUid in comp.Destinations)
|
||||
{
|
||||
var dest = Comp<GatewayDestinationComponent>(destUid);
|
||||
if (!dest.Enabled)
|
||||
continue;
|
||||
|
||||
destinations.Add((destUid, dest.Name, dest.NextReady, HasComp<PortalComponent>(destUid)));
|
||||
}
|
||||
|
||||
GetDestination(uid, out var current);
|
||||
var state = new GatewayBoundUserInterfaceState(destinations, current, comp.NextClose, comp.LastOpen);
|
||||
_ui.TrySetUiState(uid, GatewayUiKey.Key, state);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid uid)
|
||||
{
|
||||
_appearance.SetData(uid, GatewayVisuals.Active, HasComp<PortalComponent>(uid));
|
||||
}
|
||||
|
||||
private void OnOpenPortal(EntityUid uid, GatewayComponent comp, GatewayOpenPortalMessage args)
|
||||
{
|
||||
// can't link if portal is already open on either side, the destination is invalid or on cooldown
|
||||
if (HasComp<PortalComponent>(uid) ||
|
||||
HasComp<PortalComponent>(args.Destination) ||
|
||||
!TryComp<GatewayDestinationComponent>(args.Destination, out var dest) ||
|
||||
!dest.Enabled ||
|
||||
_timing.CurTime < dest.NextReady)
|
||||
return;
|
||||
|
||||
// TODO: admin log???
|
||||
OpenPortal(uid, comp, args.Destination, dest);
|
||||
}
|
||||
|
||||
private void OpenPortal(EntityUid uid, GatewayComponent comp, EntityUid dest, GatewayDestinationComponent destComp)
|
||||
{
|
||||
_linkedEntity.TryLink(uid, dest);
|
||||
EnsureComp<PortalComponent>(uid);
|
||||
EnsureComp<PortalComponent>(dest);
|
||||
|
||||
// for ui
|
||||
comp.LastOpen = _timing.CurTime;
|
||||
// close automatically after time is up
|
||||
comp.NextClose = comp.LastOpen + destComp.OpenTime;
|
||||
|
||||
_audio.PlayPvs(comp.PortalSound, uid);
|
||||
_audio.PlayPvs(comp.PortalSound, dest);
|
||||
|
||||
UpdateUserInterface(uid, comp);
|
||||
UpdateAppearance(uid);
|
||||
UpdateAppearance(dest);
|
||||
}
|
||||
|
||||
private void ClosePortal(EntityUid uid, GatewayComponent comp)
|
||||
{
|
||||
RemComp<PortalComponent>(uid);
|
||||
if (!GetDestination(uid, out var dest))
|
||||
return;
|
||||
|
||||
if (TryComp<GatewayDestinationComponent>(dest, out var destComp))
|
||||
{
|
||||
// portals closed, put it on cooldown and let it eventually be opened again
|
||||
destComp.NextReady = _timing.CurTime + destComp.Cooldown;
|
||||
}
|
||||
|
||||
_audio.PlayPvs(comp.PortalSound, uid);
|
||||
_audio.PlayPvs(comp.PortalSound, dest.Value);
|
||||
|
||||
_linkedEntity.TryUnlink(uid, dest.Value);
|
||||
RemComp<PortalComponent>(dest.Value);
|
||||
UpdateUserInterface(uid, comp);
|
||||
UpdateAppearance(uid);
|
||||
UpdateAppearance(dest.Value);
|
||||
}
|
||||
|
||||
private bool GetDestination(EntityUid uid, [NotNullWhen(true)] out EntityUid? dest)
|
||||
{
|
||||
dest = null;
|
||||
if (TryComp<LinkedEntityComponent>(uid, out var linked))
|
||||
{
|
||||
var first = linked.LinkedEntities.FirstOrDefault();
|
||||
if (first != EntityUid.Invalid)
|
||||
{
|
||||
dest = first;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnDestinationStartup(EntityUid uid, GatewayDestinationComponent comp, ComponentStartup args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<GatewayComponent>();
|
||||
while (query.MoveNext(out var gatewayUid, out var gateway))
|
||||
{
|
||||
gateway.Destinations.Add(uid);
|
||||
UpdateUserInterface(gatewayUid, gateway);
|
||||
}
|
||||
|
||||
UpdateAppearance(uid);
|
||||
}
|
||||
|
||||
private void OnDestinationShutdown(EntityUid uid, GatewayDestinationComponent comp, ComponentShutdown args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<GatewayComponent>();
|
||||
while (query.MoveNext(out var gatewayUid, out var gateway))
|
||||
{
|
||||
gateway.Destinations.Remove(uid);
|
||||
UpdateUserInterface(gatewayUid, gateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,25 +25,29 @@ public sealed partial class GatherableSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GatherableComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<GatherableComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<GatherableComponent, GatherableDoAfterEvent>(OnDoAfter);
|
||||
InitializeProjectile();
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, GatherableComponent component, InteractUsingEvent args)
|
||||
private void Gather(EntityUid gatheredUid, EntityUid user, EntityUid used, GatheringToolComponent? tool = null, GatherableComponent? component = null)
|
||||
{
|
||||
if (!TryComp<GatheringToolComponent>(args.Used, out var tool) || component.ToolWhitelist?.IsValid(args.Used) == false)
|
||||
if (!Resolve(used, ref tool, false) || !Resolve(gatheredUid, ref component, false) ||
|
||||
component.ToolWhitelist?.IsValid(used) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't gather too many entities at once.
|
||||
if (tool.MaxGatheringEntities < tool.GatheringEntities.Count + 1)
|
||||
return;
|
||||
|
||||
var damageRequired = _destructible.DestroyedAt(uid);
|
||||
var damageRequired = _destructible.DestroyedAt(gatheredUid);
|
||||
var damageTime = (damageRequired / tool.Damage.Total).Float();
|
||||
damageTime = Math.Max(1f, damageTime);
|
||||
|
||||
var doAfter = new DoAfterArgs(args.User, damageTime, new GatherableDoAfterEvent(), uid, target: uid, used: args.Used)
|
||||
var doAfter = new DoAfterArgs(user, damageTime, new GatherableDoAfterEvent(), gatheredUid, target: gatheredUid, used: used)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnTargetMove = true,
|
||||
@@ -54,6 +58,16 @@ public sealed partial class GatherableSystem : EntitySystem
|
||||
_doAfterSystem.TryStartDoAfter(doAfter);
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, GatherableComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
Gather(uid, args.User, args.User);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, GatherableComponent component, InteractUsingEvent args)
|
||||
{
|
||||
Gather(uid, args.User, args.Used, component: component);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, GatherableComponent component, GatherableDoAfterEvent args)
|
||||
{
|
||||
if(!TryComp<GatheringToolComponent>(args.Args.Used, out var tool))
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Content.Server.Ghost.Roles.Components
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public string RoleRules
|
||||
{
|
||||
get => _roleRules;
|
||||
get => Loc.GetString(_roleRules);
|
||||
set
|
||||
{
|
||||
_roleRules = value;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user