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:
Morb0
2023-08-02 15:13:51 +03:00
553 changed files with 32647 additions and 22072 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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

View File

@@ -404,7 +404,7 @@ namespace Content.Client.Construction.UI
return;
}
if (_selected == null || _selected.Mirror == String.Empty)
if (_selected == null || _selected.Mirror == null)
{
return;
}

View File

@@ -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);
}

View File

@@ -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")]

View 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);
}
}
}

View File

@@ -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()

View 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);
}
}

View 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>

View 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");
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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 -->

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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);

View 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();
}
}

View 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;
}
}

View File

@@ -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()

View 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>

View 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();
}
}

View File

@@ -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));
}
}

View File

@@ -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()
{

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -1,5 +1,7 @@
using Content.Shared.Salvage;
using Robust.Shared.GameStates;
namespace Content.Client.Salvage;
[NetworkedComponent, RegisterComponent]
public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent {}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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)

View File

@@ -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;

View File

@@ -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,

View File

@@ -157,7 +157,11 @@ public sealed class SaveLoadReparentTest
Assert.That(component.ParentSlot.Child, Is.EqualTo(id));
});
}
maps.DeleteMap(mapId);
}
});
await pairTracker.CleanReturnAsync();
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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]

View File

@@ -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();
}
}
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -150,7 +150,7 @@ namespace Content.IntegrationTests.Tests
var mapNames = new List<string>();
var naughty = new HashSet<string>()
{
"Empty",
PoolManager.TestMap,
"Infiltrator",
"Pirate",
};

View File

@@ -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>();

View File

@@ -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>();

View File

@@ -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!;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}
}

View File

@@ -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");

File diff suppressed because it is too large Load Diff

View File

@@ -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)
{
}
}
}

View File

@@ -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");

View File

@@ -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; }

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -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);
}
}
}
}

View File

@@ -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++;
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -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!;

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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))

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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))

View File

@@ -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!;
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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));
}
}
}

View File

@@ -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
{
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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!;
}

View File

@@ -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>))]

View File

@@ -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);

View 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;
}

View File

@@ -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);
}

View 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);
}
}
}

View File

@@ -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))

View File

@@ -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