Merge remote-tracking branch 'space-station-14/master' into 21-04-2024-cleaning-up

# Conflicts:
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Resources/Prototypes/Maps/Pools/default.yml
#	Resources/Prototypes/Maps/europa.yml
#	Resources/Prototypes/Recipes/Lathes/electronics.yml
#	Resources/Textures/Interface/Nano/item_status_left.svg.96dpi.png
#	Resources/Textures/Interface/Nano/item_status_middle.svg.96dpi.png
#	Resources/Textures/Interface/Nano/item_status_right.svg.96dpi.png
This commit is contained in:
Ed
2024-04-21 20:54:50 +03:00
761 changed files with 202329 additions and 28651 deletions

View File

@@ -9,7 +9,7 @@ indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf:suggestion
#end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
@@ -104,7 +104,6 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
csharp_style_namespace_declarations = file_scoped:suggestion
#### C# Formatting Rules ####

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -46,7 +46,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" };
public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry", "Oasis" };
[ParamsSource(nameof(MapsSource))]
public string Map;

View File

@@ -126,12 +126,15 @@ namespace Content.Client.Administration.Managers
public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
{
return uid == _player.LocalEntity ? _adminData : null;
if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin))
return _adminData;
return null;
}
public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false)
{
if (_player.LocalUser == session.UserId)
if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin))
return _adminData;
return null;

View File

@@ -3,6 +3,7 @@ using System.Net;
using System.Net.Sockets;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
@@ -11,6 +12,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -32,8 +34,11 @@ public sealed partial class BanPanel : DefaultWindow
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
private readonly List<CheckBox> _roleCheckboxes = new();
private readonly ISawmill _banpanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private enum TabNumbers
{
@@ -65,6 +70,7 @@ public sealed partial class BanPanel : DefaultWindow
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ =>
@@ -104,6 +110,11 @@ public sealed partial class BanPanel : DefaultWindow
};
SubmitButton.OnPressed += SubmitButtonOnOnPressed;
IpCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanIpBanDefault);
HwidCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanHwidBanDefault);
LastConnCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanUseLastDetails);
EraseCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanErasePlayer);
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-none"), (int) NoteSeverity.None);
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-low"), (int) NoteSeverity.Minor);
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-medium"), (int) NoteSeverity.Medium);
@@ -175,6 +186,39 @@ public sealed partial class BanPanel : DefaultWindow
c.Pressed = args.Pressed;
}
}
if (args.Pressed)
{
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
_banpanelSawmill
.Warning("Departmental role ban severity could not be parsed from config!");
return;
}
SeverityOption.SelectId((int) newSeverity);
}
else
{
foreach (var childContainer in RolesContainer.Children)
{
if (childContainer is Container)
{
foreach (var child in childContainer.Children)
{
if (child is CheckBox { Pressed: true })
return;
}
}
}
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
_banpanelSawmill
.Warning("Role ban severity could not be parsed from config!");
return;
}
SeverityOption.SelectId((int) newSeverity);
}
};
outerContainer.AddChild(innerContainer);
foreach (var role in roleList)
@@ -353,6 +397,35 @@ public sealed partial class BanPanel : DefaultWindow
{
TypeOption.ModulateSelfOverride = null;
Tabs.SetTabVisible((int) TabNumbers.Roles, TypeOption.SelectedId == (int) Types.Role);
NoteSeverity? newSeverity = null;
switch (TypeOption.SelectedId)
{
case (int)Types.Server:
if (Enum.TryParse(_cfg.GetCVar(CCVars.ServerBanDefaultSeverity), true, out NoteSeverity serverSeverity))
newSeverity = serverSeverity;
else
{
_banpanelSawmill
.Warning("Server ban severity could not be parsed from config!");
}
break;
case (int) Types.Role:
if (Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity roleSeverity))
{
newSeverity = roleSeverity;
}
else
{
_banpanelSawmill
.Warning("Role ban severity could not be parsed from config!");
}
break;
}
if (newSeverity != null)
SeverityOption.SelectId((int) newSeverity.Value);
}
private void UpdateSubmitEnabled()

View File

@@ -163,6 +163,26 @@ namespace Content.Client.Atmos.UI
parent.AddChild(panel);
panel.AddChild(dataContainer);
// Volume label
var volBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
volBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-volume-text")
});
volBox.AddChild(new Control
{
MinSize = new Vector2(10, 0),
HorizontalExpand = true
});
volBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-volume-val-text", ("volume", $"{gasMix.Volume:0.##}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
dataContainer.AddChild(volBox);
// Pressure label
var presBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };

View File

@@ -0,0 +1,119 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Audio;
using Robust.Client.Player;
using Robust.Shared.Audio.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Audio.Jukebox;
public sealed class JukeboxBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[ViewVariables]
private JukeboxMenu? _menu;
public JukeboxBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_menu = new JukeboxMenu();
_menu.OnClose += Close;
_menu.OpenCentered();
_menu.OnPlayPressed += args =>
{
if (args)
{
SendMessage(new JukeboxPlayingMessage());
}
else
{
SendMessage(new JukeboxPauseMessage());
}
};
_menu.OnStopPressed += () =>
{
SendMessage(new JukeboxStopMessage());
};
_menu.OnSongSelected += SelectSong;
_menu.SetTime += SetTime;
PopulateMusic();
Reload();
}
/// <summary>
/// Reloads the attached menu if it exists.
/// </summary>
public void Reload()
{
if (_menu == null || !EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox))
return;
_menu.SetAudioStream(jukebox.AudioStream);
if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
{
var length = EntMan.System<AudioSystem>().GetAudioLength(songProto.Path.Path.ToString());
_menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
}
else
{
_menu.SetSelectedSong(string.Empty, 0f);
}
}
public void PopulateMusic()
{
_menu?.Populate(_protoManager.EnumeratePrototypes<JukeboxPrototype>());
}
public void SelectSong(ProtoId<JukeboxPrototype> songid)
{
SendMessage(new JukeboxSelectedMessage(songid));
}
public void SetTime(float time)
{
var sentTime = time;
// You may be wondering, what the fuck is this
// Well we want to be able to predict the playback slider change, of which there are many ways to do it
// We can't just use SendPredictedMessage because it will reset every tick and audio updates every frame
// so it will go BRRRRT
// Using ping gets us close enough that it SHOULD, MOST OF THE TIME, fall within the 0.1 second tolerance
// that's still on engine so our playback position never gets corrected.
if (EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox) &&
EntMan.TryGetComponent(jukebox.AudioStream, out AudioComponent? audioComp))
{
audioComp.PlaybackPosition = time;
}
SendMessage(new JukeboxSetTimeMessage(sentTime));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (_menu == null)
return;
_menu.OnClose -= Close;
_menu.Dispose();
_menu = null;
}
}

View File

@@ -0,0 +1,18 @@
<ui:FancyWindow xmlns="https://spacestation14.io" xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="400 500" Title="{Loc 'jukebox-menu-title'}">
<BoxContainer Margin="4 0" Orientation="Vertical">
<ItemList Name="MusicList" SelectMode="Button" Margin="3 3 3 3"
HorizontalExpand="True" VerticalExpand="True" SizeFlagsStretchRatio="8"/>
<BoxContainer Orientation="Vertical">
<Label Name="SongSelected" Text="{Loc 'jukebox-menu-selectedsong'}" />
<Label Name="SongName" Text="---" />
<Slider Name="PlaybackSlider" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True"
VerticalExpand="False" SizeFlagsStretchRatio="1">
<Button Name="PlayButton" Text="{Loc 'jukebox-menu-buttonplay'}" />
<Button Name="StopButton" Text="{Loc 'jukebox-menu-buttonstop'}" />
<Label Name="DurationLabel" Text="00:00 / 00:00" HorizontalAlignment="Right" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</ui:FancyWindow>

View File

@@ -0,0 +1,166 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio.Components;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
namespace Content.Client.Audio.Jukebox;
[GenerateTypedNameReferences]
public sealed partial class JukeboxMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
private AudioSystem _audioSystem;
/// <summary>
/// Are we currently 'playing' or paused for the play / pause button.
/// </summary>
private bool _playState;
/// <summary>
/// True if playing, false if paused.
/// </summary>
public event Action<bool>? OnPlayPressed;
public event Action? OnStopPressed;
public event Action<ProtoId<JukeboxPrototype>>? OnSongSelected;
public event Action<float>? SetTime;
private EntityUid? _audio;
private float _lockTimer;
public JukeboxMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_audioSystem = _entManager.System<AudioSystem>();
MusicList.OnItemSelected += args =>
{
var entry = MusicList[args.ItemIndex];
if (entry.Metadata is not string juke)
return;
OnSongSelected?.Invoke(juke);
};
PlayButton.OnPressed += args =>
{
OnPlayPressed?.Invoke(!_playState);
};
StopButton.OnPressed += args =>
{
OnStopPressed?.Invoke();
};
PlaybackSlider.OnReleased += PlaybackSliderKeyUp;
SetPlayPauseButton(_audioSystem.IsPlaying(_audio), force: true);
}
public JukeboxMenu(AudioSystem audioSystem)
{
_audioSystem = audioSystem;
}
public void SetAudioStream(EntityUid? audio)
{
_audio = audio;
}
private void PlaybackSliderKeyUp(Slider args)
{
SetTime?.Invoke(PlaybackSlider.Value);
_lockTimer = 0.5f;
}
/// <summary>
/// Re-populates the list of jukebox prototypes available.
/// </summary>
public void Populate(IEnumerable<JukeboxPrototype> jukeboxProtos)
{
MusicList.Clear();
foreach (var entry in jukeboxProtos)
{
MusicList.AddItem(entry.Name, metadata: entry.ID);
}
}
public void SetPlayPauseButton(bool playing, bool force = false)
{
if (_playState == playing && !force)
return;
_playState = playing;
if (playing)
{
PlayButton.Text = Loc.GetString("jukebox-menu-buttonpause");
return;
}
PlayButton.Text = Loc.GetString("jukebox-menu-buttonplay");
}
public void SetSelectedSong(string name, float length)
{
SetSelectedSongText(name);
PlaybackSlider.MaxValue = length;
PlaybackSlider.SetValueWithoutEvent(0);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_lockTimer > 0f)
{
_lockTimer -= args.DeltaSeconds;
}
PlaybackSlider.Disabled = _lockTimer > 0f;
if (_entManager.TryGetComponent(_audio, out AudioComponent? audio))
{
DurationLabel.Text = $@"{TimeSpan.FromSeconds(audio.PlaybackPosition):mm\:ss} / {_audioSystem.GetAudioLength(audio.FileName):mm\:ss}";
}
else
{
DurationLabel.Text = $"00:00 / 00:00";
}
if (PlaybackSlider.Grabbed)
return;
if (audio != null || _entManager.TryGetComponent(_audio, out audio))
{
PlaybackSlider.SetValueWithoutEvent(audio.PlaybackPosition);
}
else
{
PlaybackSlider.SetValueWithoutEvent(0f);
}
SetPlayPauseButton(_audioSystem.IsPlaying(_audio, audio));
}
public void SetSelectedSongText(string? text)
{
if (!string.IsNullOrEmpty(text))
{
SongName.Text = text;
}
else
{
SongName.Text = "---";
}
}
}

View File

@@ -0,0 +1,153 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Audio.Jukebox;
public sealed class JukeboxSystem : SharedJukeboxSystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JukeboxComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<JukeboxComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<JukeboxComponent, AfterAutoHandleStateEvent>(OnJukeboxAfterState);
_protoManager.PrototypesReloaded += OnProtoReload;
}
public override void Shutdown()
{
base.Shutdown();
_protoManager.PrototypesReloaded -= OnProtoReload;
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.WasModified<JukeboxPrototype>())
return;
var query = AllEntityQuery<JukeboxComponent, UserInterfaceComponent>();
while (query.MoveNext(out _, out var ui))
{
if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
baseBui is not JukeboxBoundUserInterface bui)
{
continue;
}
bui.PopulateMusic();
}
}
private void OnJukeboxAfterState(Entity<JukeboxComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryComp(ent, out UserInterfaceComponent? ui))
return;
if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
baseBui is not JukeboxBoundUserInterface bui)
{
return;
}
bui.Reload();
}
private void OnAnimationCompleted(EntityUid uid, JukeboxComponent component, AnimationCompletedEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance) ||
!_appearanceSystem.TryGetData<JukeboxVisualState>(uid, JukeboxVisuals.VisualState, out var visualState, appearance))
{
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, sprite);
}
private void OnAppearanceChange(EntityUid uid, JukeboxComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!args.AppearanceData.TryGetValue(JukeboxVisuals.VisualState, out var visualStateObject) ||
visualStateObject is not JukeboxVisualState visualState)
{
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, args.Sprite);
}
private void UpdateAppearance(EntityUid uid, JukeboxVisualState visualState, JukeboxComponent component, SpriteComponent sprite)
{
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
switch (visualState)
{
case JukeboxVisualState.On:
SetLayerState(JukeboxVisualLayers.Base, component.OnState, sprite);
break;
case JukeboxVisualState.Off:
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
break;
case JukeboxVisualState.Select:
PlayAnimation(uid, JukeboxVisualLayers.Base, component.SelectState, 1.0f, sprite);
break;
}
}
private void PlayAnimation(EntityUid uid, JukeboxVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
if (!_animationPlayer.HasRunningAnimation(uid, state))
{
var animation = GetAnimation(layer, state, animationTime);
sprite.LayerSetVisible(layer, true);
_animationPlayer.Play(uid, animation, state);
}
}
private static Animation GetAnimation(JukeboxVisualLayers layer, string state, float animationTime)
{
return new Animation
{
Length = TimeSpan.FromSeconds(animationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = layer,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
}
}
}
};
}
private void SetLayerState(JukeboxVisualLayers layer, string? state, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
sprite.LayerSetVisible(layer, true);
sprite.LayerSetAutoAnimated(layer, true);
sprite.LayerSetState(layer, state);
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Body.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
using Content.Shared.Examine;
@@ -13,9 +14,14 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
private EntityQuery<BodyComponent> _bodyQuery;
public override void Initialize()
{
base.Initialize();
_bodyQuery = GetEntityQuery<BodyComponent>();
SubscribeNetworkEvent<PlayBoxEffectMessage>(OnBoxEffect);
}
@@ -59,6 +65,10 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
if (!_examine.InRangeUnOccluded(sourcePos, mapPos, box.Distance, null))
continue;
// no effect for anything too exotic
if (!_bodyQuery.HasComp(mob))
continue;
var ent = Spawn(box.Effect, mapPos);
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))

View File

@@ -0,0 +1,22 @@
using Content.Client.Chemistry.EntitySystems;
using Content.Client.Chemistry.UI;
namespace Content.Client.Chemistry.Components;
/// <summary>
/// Exposes a solution container's contents via a basic item status control.
/// </summary>
/// <remarks>
/// Shows the solution volume, max volume, and transfer amount.
/// </remarks>
/// <seealso cref="SolutionItemStatusSystem"/>
/// <seealso cref="SolutionStatusControl"/>
[RegisterComponent]
public sealed partial class SolutionItemStatusComponent : Component
{
/// <summary>
/// The ID of the solution that will be shown on the item status control.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string Solution = "default";
}

View File

@@ -0,0 +1,22 @@
using Content.Client.Chemistry.Components;
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
/// <summary>
/// Wires up item status logic for <see cref="SolutionItemStatusComponent"/>.
/// </summary>
/// <seealso cref="SolutionStatusControl"/>
public sealed class SolutionItemStatusSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<SolutionItemStatusComponent>(
entity => new SolutionStatusControl(entity, EntityManager, _solutionContainerSystem));
}
}

View File

@@ -0,0 +1,59 @@
using Content.Client.Chemistry.Components;
using Content.Client.Chemistry.EntitySystems;
using Content.Client.Items.UI;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Chemistry.UI;
/// <summary>
/// Displays basic solution information for <see cref="SolutionItemStatusComponent"/>.
/// </summary>
/// <seealso cref="SolutionItemStatusSystem"/>
public sealed class SolutionStatusControl : PollingItemStatusControl<SolutionStatusControl.Data>
{
private readonly Entity<SolutionItemStatusComponent> _parent;
private readonly IEntityManager _entityManager;
private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
public SolutionStatusControl(
Entity<SolutionItemStatusComponent> parent,
IEntityManager entityManager,
SharedSolutionContainerSystem solutionContainers)
{
_parent = parent;
_entityManager = entityManager;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
}
protected override Data PollData()
{
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.Solution, out _, out var solution))
return default;
FixedPoint2? transferAmount = null;
if (_entityManager.TryGetComponent(_parent.Owner, out SolutionTransferComponent? transfer))
transferAmount = transfer.TransferAmount;
return new Data(solution.Volume, solution.MaxVolume, transferAmount);
}
protected override void Update(in Data data)
{
var markup = Loc.GetString("solution-status-volume",
("currentVolume", data.Volume),
("maxVolume", data.MaxVolume));
if (data.TransferVolume is { } transferVolume)
markup += "\n" + Loc.GetString("solution-status-transfer", ("volume", transferVolume));
_label.SetMarkup(markup);
}
public readonly record struct Data(FixedPoint2 Volume, FixedPoint2 MaxVolume, FixedPoint2? TransferVolume);
}

View File

@@ -1,7 +1,9 @@
using Content.Client.UserInterface.Controls;
using System.Threading;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -13,6 +15,8 @@ namespace Content.Client.Communications.UI
private CommunicationsConsoleBoundUserInterface Owner { get; set; }
private readonly CancellationTokenSource _timerCancelTokenSource = new();
[Dependency] private readonly IConfigurationManager _cfg = default!;
public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner)
{
IoCManager.InjectDependencies(this);
@@ -23,6 +27,22 @@ namespace Content.Client.Communications.UI
var loc = IoCManager.Resolve<ILocalizationManager>();
MessageInput.Placeholder = new Rope.Leaf(loc.GetString("comms-console-menu-announcement-placeholder"));
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
MessageInput.OnTextChanged += (args) =>
{
if (args.Control.TextLength > maxAnnounceLength)
{
AnnounceButton.Disabled = true;
AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long");
}
else
{
AnnounceButton.Disabled = !owner.CanAnnounce;
AnnounceButton.ToolTip = null;
}
};
AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(Rope.Collapse(MessageInput.TextRope));
AnnounceButton.Disabled = !owner.CanAnnounce;

View File

@@ -1,3 +1,4 @@
using Content.Client.Administration.Managers;
using Content.Client.Gameplay;
using Content.Client.Lobby;
using Content.Client.RoundEnd;
@@ -14,7 +15,9 @@ namespace Content.Client.GameTicking.Managers
public sealed class ClientGameTicker : SharedGameTicker
{
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[ViewVariables] private bool _initialized;
private Dictionary<NetEntity, Dictionary<string, uint?>> _jobsAvailable = new();
@@ -44,8 +47,6 @@ namespace Content.Client.GameTicking.Managers
public override void Initialize()
{
DebugTools.Assert(!_initialized);
SubscribeNetworkEvent<TickerJoinLobbyEvent>(JoinLobby);
SubscribeNetworkEvent<TickerJoinGameEvent>(JoinGame);
SubscribeNetworkEvent<TickerConnectionStatusEvent>(ConnectionStatus);
@@ -53,14 +54,33 @@ namespace Content.Client.GameTicking.Managers
SubscribeNetworkEvent<TickerLobbyInfoEvent>(LobbyInfo);
SubscribeNetworkEvent<TickerLobbyCountdownEvent>(LobbyCountdown);
SubscribeNetworkEvent<RoundEndMessageEvent>(RoundEnd);
SubscribeNetworkEvent<RequestWindowAttentionEvent>(msg =>
{
IoCManager.Resolve<IClyde>().RequestWindowAttention();
});
SubscribeNetworkEvent<RequestWindowAttentionEvent>(OnAttentionRequest);
SubscribeNetworkEvent<TickerLateJoinStatusEvent>(LateJoinStatus);
SubscribeNetworkEvent<TickerJobsAvailableEvent>(UpdateJobsAvailable);
_initialized = true;
_admin.AdminStatusUpdated += OnAdminUpdated;
OnAdminUpdated();
}
public override void Shutdown()
{
_admin.AdminStatusUpdated -= OnAdminUpdated;
base.Shutdown();
}
private void OnAdminUpdated()
{
// Hide some map/grid related logs from clients. This is to try prevent some easy metagaming by just
// reading the console. E.g., logs like this one could leak the nuke station/grid:
// > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470)
#if !DEBUG
_map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
#endif
}
private void OnAttentionRequest(RequestWindowAttentionEvent ev)
{
_clyde.RequestWindowAttention();
}
private void LateJoinStatus(TickerLateJoinStatusEvent message)
@@ -137,7 +157,7 @@ namespace Content.Client.GameTicking.Managers
return;
//This is not ideal at all, but I don't see an immediately better fit anywhere else.
_window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, _entityManager);
_window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, EntityManager);
}
}
}

View File

@@ -42,7 +42,7 @@ public class GuideEntry
}
[Prototype("guideEntry")]
public sealed class GuideEntryPrototype : GuideEntry, IPrototype
public sealed partial class GuideEntryPrototype : GuideEntry, IPrototype
{
public string ID => Id;
}

View File

@@ -199,7 +199,7 @@ namespace Content.Client.Inventory
public void UIInventoryStorageActivate(string slot)
{
EntityManager.EntityNetManager?.SendSystemNetworkMessage(new OpenSlotStorageNetworkMessage(slot));
EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
}
public void UIInventoryExamine(string slot, EntityUid uid)
@@ -251,6 +251,7 @@ namespace Content.Client.Inventory
public string SlotGroup => SlotDef.SlotGroup;
public string SlotDisplayName => SlotDef.DisplayName;
public string TextureName => "Slots/" + SlotDef.TextureName;
public string FullTextureName => SlotDef.FullTextureName;
public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false,
bool blocked = false)

View File

@@ -219,7 +219,7 @@ namespace Content.Client.Inventory
if (entity == null)
{
button.SpriteView.SetEntity(null);
button.SetEntity(null);
return;
}
@@ -231,7 +231,7 @@ namespace Content.Client.Inventory
else
return;
button.SpriteView.SetEntity(viewEnt);
button.SetEntity(viewEnt);
}
}
}

View File

@@ -0,0 +1,28 @@
using Robust.Client.UserInterface;
using Robust.Shared.Timing;
namespace Content.Client.Items.UI;
/// <summary>
/// A base for item status controls that poll data every frame. Avoids UI updates if data didn't change.
/// </summary>
/// <typeparam name="TData">The full status control data that is polled every frame.</typeparam>
public abstract class PollingItemStatusControl<TData> : Control where TData : struct, IEquatable<TData>
{
private TData _lastData;
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var newData = PollData();
if (newData.Equals(_lastData))
return;
_lastData = newData;
Update(newData);
}
protected abstract TData PollData();
protected abstract void Update(in TData data);
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Client.Nutrition.EntitySystems;
public sealed class DrinkSystem : SharedDrinkSystem
{
}

View File

@@ -36,6 +36,9 @@
<CheckBox Name="IntegerScalingCheckBox"
Text="{Loc 'ui-options-vp-integer-scaling'}"
ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" />
<CheckBox Name="ViewportVerticalFitCheckBox"
Text="{Loc 'ui-options-vp-vertical-fit'}"
ToolTip="{Loc 'ui-options-vp-vertical-fit-tooltip'}" />
<CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
<CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
<CheckBox Name="FpsCounterCheckBox" Text="{Loc 'ui-options-fps-counter'}" />

View File

@@ -67,6 +67,12 @@ namespace Content.Client.Options.UI.Tabs
UpdateApplyButton();
};
ViewportVerticalFitCheckBox.OnToggled += _ =>
{
UpdateViewportScale();
UpdateApplyButton();
};
IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled;
ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled;
@@ -79,6 +85,7 @@ namespace Content.Client.Options.UI.Tabs
ViewportScaleSlider.Value = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
ViewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch);
IntegerScalingCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0;
ViewportVerticalFitCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportVerticalFit);
ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender);
ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality);
FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -111,6 +118,7 @@ namespace Content.Client.Options.UI.Tabs
_cfg.SetCVar(CCVars.ViewportFixedScaleFactor, (int) ViewportScaleSlider.Value);
_cfg.SetCVar(CCVars.ViewportSnapToleranceMargin,
IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0);
_cfg.SetCVar(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed);
_cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed);
_cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed);
@@ -140,6 +148,7 @@ namespace Content.Client.Options.UI.Tabs
var isVPStretchSame = ViewportStretchCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportStretch);
var isVPScaleSame = (int) ViewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0);
var isVPVerticalFitSame = ViewportVerticalFitCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportVerticalFit);
var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender);
var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality);
var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -152,6 +161,7 @@ namespace Content.Client.Options.UI.Tabs
isVPStretchSame &&
isVPScaleSame &&
isIntegerScalingSame &&
isVPVerticalFitSame &&
isVPResSame &&
isPLQSame &&
isFpsCounterVisibleSame &&
@@ -235,6 +245,8 @@ namespace Content.Client.Options.UI.Tabs
{
ViewportScaleBox.Visible = !ViewportStretchCheckBox.Pressed;
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportWidthSlider.Visible = ViewportWidthSliderDisplay.Visible = !ViewportStretchCheckBox.Pressed || ViewportStretchCheckBox.Pressed && !ViewportVerticalFitCheckBox.Pressed;
ViewportScaleText.Text = Loc.GetString("ui-options-vp-scale", ("scale", ViewportScaleSlider.Value));
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Overlays;
using Content.Shared.Security.Components;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowCriminalRecordIconsSystem : EquipmentHudSystem<ShowCriminalRecordIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CriminalRecordComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
return;
if (_prototype.TryIndex<StatusIconPrototype>(component.StatusIcon.Id, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -1,14 +1,13 @@
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowHungerIconsSystem : EquipmentHudSystem<ShowHungerIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeMan = default!;
[Dependency] private readonly HungerSystem _hunger = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public sealed class ShowHungerIconsSystem : EquipmentHudSystem<ShowHungerIconsCo
SubscribeLocalEvent<HungerComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent hungerComponent, ref GetStatusIconsEvent args)
private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || args.InContainer)
if (!IsActive || ev.InContainer)
return;
var hungerIcons = DecideHungerIcon(uid, hungerComponent);
args.StatusIcons.AddRange(hungerIcons);
}
private IReadOnlyList<StatusIconPrototype> DecideHungerIcon(EntityUid uid, HungerComponent hungerComponent)
{
var result = new List<StatusIconPrototype>();
switch (hungerComponent.CurrentThreshold)
{
case HungerThreshold.Overfed:
if (_prototypeMan.TryIndex<StatusIconPrototype>("HungerIconOverfed", out var overfed))
{
result.Add(overfed);
}
break;
case HungerThreshold.Peckish:
if (_prototypeMan.TryIndex<StatusIconPrototype>("HungerIconPeckish", out var peckish))
{
result.Add(peckish);
}
break;
case HungerThreshold.Starving:
if (_prototypeMan.TryIndex<StatusIconPrototype>("HungerIconStarving", out var starving))
{
result.Add(starving);
}
break;
}
return result;
if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -0,0 +1,60 @@
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 ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = 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 ev)
{
if (!IsActive || ev.InContainer)
return;
var iconId = JobIconForNoId;
if (_accessReader.FindAccessItemsInventory(uid, out var items))
{
foreach (var item in items)
{
// ID Card
if (TryComp<IdCardComponent>(item, out var id))
{
iconId = id.JobIcon;
break;
}
// PDA
if (TryComp<PdaComponent>(item, out var pda)
&& pda.ContainedId != null
&& TryComp(pda.ContainedId, out id))
{
iconId = id.JobIcon;
break;
}
}
}
if (_prototype.TryIndex<StatusIconPrototype>(iconId, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
else
Log.Error($"Invalid job icon prototype: {iconPrototype}");
}
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Mindshield.Components;
using Content.Shared.Overlays;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShieldIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindShieldComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
return;
if (_prototype.TryIndex<StatusIconPrototype>(component.MindShieldStatusIcon.Id, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -1,86 +0,0 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Mindshield.Components;
using Content.Shared.Overlays;
using Content.Shared.PDA;
using Content.Shared.Security.Components;
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 securityIcons = DecideSecurityIcon(uid);
@event.StatusIcons.AddRange(securityIcons);
}
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}");
if (TryComp<MindShieldComponent>(uid, out var comp))
{
if (_prototypeMan.TryIndex<StatusIconPrototype>(comp.MindShieldStatusIcon.Id, out var icon))
result.Add(icon);
}
if (TryComp<CriminalRecordComponent>(uid, out var record))
{
if(_prototypeMan.TryIndex<StatusIconPrototype>(record.StatusIcon.Id, out var criminalIcon))
result.Add(criminalIcon);
}
return result;
}
}

View File

@@ -1,10 +1,11 @@
using Content.Shared.Overlays;
using Content.Shared.StatusIcon.Components;
using Content.Shared.NukeOps;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem<ShowSyndicateIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -16,28 +17,13 @@ public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem<ShowSyndicateI
SubscribeLocalEvent<NukeOperativeComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent nukeOperativeComponent, ref GetStatusIconsEvent args)
private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || args.InContainer)
{
if (!IsActive || ev.InContainer)
return;
}
var syndicateIcons = SyndicateIcon(uid, nukeOperativeComponent);
args.StatusIcons.AddRange(syndicateIcons);
}
private IReadOnlyList<StatusIconPrototype> SyndicateIcon(EntityUid uid, NukeOperativeComponent nukeOperativeComponent)
{
var result = new List<StatusIconPrototype>();
if (_prototype.TryIndex<StatusIconPrototype>(nukeOperativeComponent.SyndStatusIcon, out var syndicateicon))
{
result.Add(syndicateicon);
}
return result;
if (_prototype.TryIndex<StatusIconPrototype>(component.SyndStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -1,14 +1,13 @@
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeMan = default!;
[Dependency] private readonly ThirstSystem _thirst = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsCo
SubscribeLocalEvent<ThirstComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent thirstComponent, ref GetStatusIconsEvent args)
private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || args.InContainer)
if (!IsActive || ev.InContainer)
return;
var thirstIcons = DecideThirstIcon(uid, thirstComponent);
args.StatusIcons.AddRange(thirstIcons);
}
private IReadOnlyList<StatusIconPrototype> DecideThirstIcon(EntityUid uid, ThirstComponent thirstComponent)
{
var result = new List<StatusIconPrototype>();
switch (thirstComponent.CurrentThirstThreshold)
{
case ThirstThreshold.OverHydrated:
if (_prototypeMan.TryIndex<StatusIconPrototype>("ThirstIconOverhydrated", out var overhydrated))
{
result.Add(overhydrated);
}
break;
case ThirstThreshold.Thirsty:
if (_prototypeMan.TryIndex<StatusIconPrototype>("ThirstIconThirsty", out var thirsty))
{
result.Add(thirsty);
}
break;
case ThirstThreshold.Parched:
if (_prototypeMan.TryIndex<StatusIconPrototype>("ThirstIconParched", out var parched))
{
result.Add(parched);
}
break;
}
return result;
if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype!);
}
}

View File

@@ -1,18 +1,14 @@
using System.Numerics;
using Content.Shared.Pinpointer;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.Pinpointer;
public sealed class NavMapSystem : SharedNavMapSystem
public sealed partial class NavMapSystem : SharedNavMapSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NavMapComponent, ComponentHandleState>(OnHandleState);
}
@@ -21,89 +17,47 @@ public sealed class NavMapSystem : SharedNavMapSystem
if (args.Current is not NavMapComponentState state)
return;
component.Chunks.Clear();
foreach (var (origin, data) in state.TileData)
if (!state.FullState)
{
component.Chunks.Add(origin, new NavMapChunk(origin)
foreach (var index in component.Chunks.Keys)
{
TileData = data,
});
}
if (!state.AllChunks!.Contains(index))
component.Chunks.Remove(index);
}
component.Beacons.Clear();
component.Beacons.AddRange(state.Beacons);
component.Airlocks.Clear();
component.Airlocks.AddRange(state.Airlocks);
}
}
public sealed class NavMapOverlay : Overlay
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private List<Entity<MapGridComponent>> _grids = new();
public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
{
_entManager = entManager;
_mapManager = mapManager;
}
protected override void Draw(in OverlayDrawArgs args)
{
var query = _entManager.GetEntityQuery<NavMapComponent>();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
foreach (var grid in _grids)
{
if (!query.TryGetComponent(grid, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
continue;
// TODO: Faster helper method
var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
var localAABB = invMatrix.TransformBox(args.WorldBounds);
Matrix3.Multiply(in scale, in matrix, out var matty);
args.WorldHandle.SetTransform(matty);
for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
foreach (var beacon in component.Beacons)
{
for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
{
var floored = new Vector2i((int) x, (int) y);
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
if (mask == 0x0)
continue;
var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
args.WorldHandle.DrawRect(new Box2(tile * grid.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false);
}
}
if (!state.AllBeacons!.Contains(beacon))
component.Beacons.Remove(beacon);
}
}
args.WorldHandle.SetTransform(Matrix3.Identity);
else
{
foreach (var index in component.Chunks.Keys)
{
if (!state.Chunks.ContainsKey(index))
component.Chunks.Remove(index);
}
foreach (var beacon in component.Beacons)
{
if (!state.Beacons.Contains(beacon))
component.Beacons.Remove(beacon);
}
}
foreach (var ((category, origin), chunk) in state.Chunks)
{
var newChunk = new NavMapChunk(origin);
foreach (var (atmosDirection, value) in chunk)
newChunk.TileData[atmosDirection] = value;
component.Chunks[(category, origin)] = newChunk;
}
foreach (var beacon in state.Beacons)
component.Beacons.Add(beacon);
}
}

View File

@@ -16,6 +16,8 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
using Content.Shared.Atmos;
using System.Linq;
namespace Content.Client.Pinpointer.UI;
@@ -27,6 +29,7 @@ public partial class NavMapControl : MapGridControl
{
[Dependency] private IResourceCache _cache = default!;
private readonly SharedTransformSystem _transformSystem;
private readonly SharedNavMapSystem _navMapSystem;
public EntityUid? Owner;
public EntityUid? MapUid;
@@ -40,7 +43,10 @@ public partial class NavMapControl : MapGridControl
// Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
public Dictionary<Vector2i, List<NavMapLine>>? TileGrid = default!;
public List<(Vector2, Vector2)> TileLines = new();
public List<(Vector2, Vector2)> TileRects = new();
public List<(Vector2[], Color)> TilePolygons = new();
// Default colors
public Color WallColor = new(102, 217, 102);
@@ -53,14 +59,23 @@ public partial class NavMapControl : MapGridControl
protected static float MinDisplayedRange = 8f;
protected static float MaxDisplayedRange = 128f;
protected static float DefaultDisplayedRange = 48f;
protected float MinmapScaleModifier = 0.075f;
protected float FullWallInstep = 0.165f;
protected float ThinWallThickness = 0.165f;
protected float ThinDoorThickness = 0.30f;
// Local variables
private float _updateTimer = 0.25f;
private float _updateTimer = 1.0f;
private Dictionary<Color, Color> _sRGBLookUp = new();
protected Color BackgroundColor;
protected float BackgroundOpacity = 0.9f;
private int _targetFontsize = 8;
protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookup = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookupReversed = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookup = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookupReversed = new();
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
@@ -72,6 +87,7 @@ public partial class NavMapControl : MapGridControl
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
HorizontalExpand = true,
Margin = new Thickness(8f, 8f),
};
@@ -80,6 +96,7 @@ public partial class NavMapControl : MapGridControl
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
HorizontalExpand = true,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
@@ -87,9 +104,10 @@ public partial class NavMapControl : MapGridControl
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
HorizontalExpand = true,
Margin = new Thickness(4f, 0f),
Pressed = true,
};
@@ -98,6 +116,8 @@ public partial class NavMapControl : MapGridControl
IoCManager.InjectDependencies(this);
_transformSystem = EntManager.System<SharedTransformSystem>();
_navMapSystem = EntManager.System<SharedNavMapSystem>();
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
@@ -112,6 +132,8 @@ public partial class NavMapControl : MapGridControl
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
HorizontalExpand = true,
SetWidth = 650f,
Children =
{
new BoxContainer()
@@ -130,6 +152,7 @@ public partial class NavMapControl : MapGridControl
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
Children =
{
topPanel,
@@ -157,6 +180,9 @@ public partial class NavMapControl : MapGridControl
{
EntManager.TryGetComponent(MapUid, out _navMap);
EntManager.TryGetComponent(MapUid, out _grid);
EntManager.TryGetComponent(MapUid, out _xform);
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
UpdateNavMap();
}
@@ -251,119 +277,93 @@ public partial class NavMapControl : MapGridControl
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
if (_navMap == null || _grid == null || _xform == null)
return;
// Map re-centering
_recenter.Disabled = DrawRecenter();
_zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
if (_navMap == null || _xform == null)
return;
// Update zoom text
_zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange):0.0}"));
// Update offset with physics local center
var offset = Offset;
if (_physics != null)
offset += _physics.LocalCenter;
// Draw tiles
if (_fixtures != null)
var offsetVec = new Vector2(offset.X, -offset.Y);
// Wall sRGB
if (!_sRGBLookUp.TryGetValue(WallColor, out var wallsRGB))
{
wallsRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = wallsRGB;
}
// Draw floor tiles
if (TilePolygons.Any())
{
Span<Vector2> verts = new Vector2[8];
foreach (var fixture in _fixtures.Fixtures.Values)
foreach (var (polygonVerts, polygonColor) in TilePolygons)
{
if (fixture.Shape is not PolygonShape poly)
continue;
for (var i = 0; i < poly.VertexCount; i++)
for (var i = 0; i < polygonVerts.Length; i++)
{
var vert = poly.Vertices[i] - offset;
var vert = polygonVerts[i] - offset;
verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..polygonVerts.Length], polygonColor);
}
}
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
// Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
// to figure out where they should be drawn. However, we don't *need* to do check these every frame.
// Instead, lets periodically update where to draw each line and then store these points in a list.
// Then we can just run through the list each frame and draw the lines without any extra computation.
// Draw walls
if (TileGrid != null && TileGrid.Count > 0)
// Draw map lines
if (TileLines.Any())
{
var walls = new ValueList<Vector2>();
var lines = new ValueList<Vector2>(TileLines.Count * 2);
foreach ((var chunk, var chunkedLines) in TileGrid)
foreach (var (o, t) in TileLines)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
var origin = ScalePosition(o - offsetVec);
var terminus = ScalePosition(t - offsetVec);
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
foreach (var chunkedLine in chunkedLines)
{
var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
walls.Add(start);
walls.Add(end);
}
lines.Add(origin);
lines.Add(terminus);
}
if (walls.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
}
if (lines.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, wallsRGB);
}
var airlockBuffer = Vector2.One * (MinimapScale / 2.25f) * 0.75f;
var airlockLines = new ValueList<Vector2>();
var foobarVec = new Vector2(1, -1);
foreach (var airlock in _navMap.Airlocks)
// Draw map rects
if (TileRects.Any())
{
var position = airlock.Position - offset;
position = ScalePosition(position with { Y = -position.Y });
airlockLines.Add(position + airlockBuffer);
airlockLines.Add(position - airlockBuffer * foobarVec);
var rects = new ValueList<Vector2>(TileRects.Count * 8);
airlockLines.Add(position + airlockBuffer);
airlockLines.Add(position + airlockBuffer * foobarVec);
airlockLines.Add(position - airlockBuffer);
airlockLines.Add(position + airlockBuffer * foobarVec);
airlockLines.Add(position - airlockBuffer);
airlockLines.Add(position - airlockBuffer * foobarVec);
airlockLines.Add(position + airlockBuffer * -Vector2.UnitY);
airlockLines.Add(position - airlockBuffer * -Vector2.UnitY);
}
if (airlockLines.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
foreach (var (lt, rb) in TileRects)
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
var leftTop = ScalePosition(lt - offsetVec);
var rightBottom = ScalePosition(rb - offsetVec);
var rightTop = new Vector2(rightBottom.X, leftTop.Y);
var leftBottom = new Vector2(leftTop.X, rightBottom.Y);
rects.Add(leftTop);
rects.Add(rightTop);
rects.Add(rightTop);
rects.Add(rightBottom);
rects.Add(rightBottom);
rects.Add(leftBottom);
rects.Add(leftBottom);
rects.Add(leftTop);
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, airlockLines.Span, sRGB);
if (rects.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, rects.Span, wallsRGB);
}
// Invoke post wall drawing action
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
@@ -373,7 +373,7 @@ public partial class NavMapControl : MapGridControl
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize , 0);
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
foreach (var beacon in _navMap.Beacons)
@@ -409,8 +409,6 @@ public partial class NavMapControl : MapGridControl
}
// Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
var iconVertexUVs = new Dictionary<(Texture, Color), ValueList<DrawVertexUV2D>>();
foreach (var blip in TrackedEntities.Values)
{
if (blip.Blinks && !lit)
@@ -419,9 +417,6 @@ public partial class NavMapControl : MapGridControl
if (blip.Texture == null)
continue;
if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
vertexUVs = new();
var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
@@ -429,29 +424,11 @@ public partial class NavMapControl : MapGridControl
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = ScalePosition(new Vector2(position.X, -position.Y));
var scalingCoefficient = 2.5f;
var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
}
foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
{
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
}
}
@@ -469,124 +446,294 @@ public partial class NavMapControl : MapGridControl
}
protected virtual void UpdateNavMap()
{
// Clear stale values
TilePolygons.Clear();
TileLines.Clear();
TileRects.Clear();
UpdateNavMapFloorTiles();
UpdateNavMapWallLines();
UpdateNavMapAirlocks();
}
private void UpdateNavMapFloorTiles()
{
if (_fixtures == null)
return;
var verts = new Vector2[8];
foreach (var fixture in _fixtures.Fixtures.Values)
{
if (fixture.Shape is not PolygonShape poly)
continue;
for (var i = 0; i < poly.VertexCount; i++)
{
var vert = poly.Vertices[i];
verts[i] = new Vector2(MathF.Round(vert.X), MathF.Round(vert.Y));
}
TilePolygons.Add((verts[..poly.VertexCount], TileColor));
}
}
private void UpdateNavMapWallLines()
{
if (_navMap == null || _grid == null)
return;
TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
}
// We'll use the following dictionaries to combine collinear wall lines
HorizLinesLookup.Clear();
HorizLinesLookupReversed.Clear();
VertLinesLookup.Clear();
VertLinesLookupReversed.Clear();
public Dictionary<Vector2i, List<NavMapLine>> GetDecodedWallChunks
(Dictionary<Vector2i, NavMapChunk> chunks,
MapGridComponent grid)
{
var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
foreach ((var chunkOrigin, var chunk) in chunks)
foreach ((var (category, chunkOrigin), var chunk) in _navMap.Chunks)
{
var list = new List<NavMapLine>();
if (category != NavMapChunkType.Wall)
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
var value = (ushort) Math.Pow(2, i);
var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
if (mask == 0x0)
continue;
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relativeTile))
{
AddRectForThinWall(chunk.TileData, tile);
continue;
}
tile = tile with { Y = -tile.Y };
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, 1)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.South] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.South] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, -_grid.TileSize), HorizLinesLookup, HorizLinesLookupReversed);
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(1, 0)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.West] &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.West] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(_grid.TileSize, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, 0), VertLinesLookup, VertLinesLookupReversed);
// South edge
if (relativeTile.Y == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, -1)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.North] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.North] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
}
AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed);
// West edge
if (relativeTile.X == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(-1, 0)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.East] &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.East] & flag) != 0x0;
}
if (!neighbor)
{
// Add point
list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed);
// Draw a diagonal line for interiors.
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
// Add a diagonal line for interiors. Unless there are a lot of double walls, there is no point combining these
TileLines.Add((tile + new Vector2(0, -_grid.TileSize), tile + new Vector2(_grid.TileSize, 0)));
}
decodedOutput.Add(chunkOrigin, list);
}
return decodedOutput;
// Record the combined lines
foreach (var (origin, terminal) in HorizLinesLookup)
TileLines.Add((origin.Item2, terminal.Item2));
foreach (var (origin, terminal) in VertLinesLookup)
TileLines.Add((origin.Item2, terminal.Item2));
}
private void UpdateNavMapAirlocks()
{
if (_navMap == null || _grid == null)
return;
foreach (var ((category, _), chunk) in _navMap.Chunks)
{
if (category != NavMapChunkType.Airlock)
continue;
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
if (mask == 0x0)
continue;
var relative = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relative) * _grid.TileSize;
// If the edges of an airlock tile are not all occupied, draw a thin airlock for each edge
if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relative))
{
AddRectForThinAirlock(chunk.TileData, tile);
continue;
}
// Otherwise add a single full tile airlock
TileRects.Add((new Vector2(tile.X + FullWallInstep, -tile.Y - FullWallInstep),
new Vector2(tile.X - FullWallInstep + 1f, -tile.Y + FullWallInstep - 1)));
TileLines.Add((new Vector2(tile.X + 0.5f, -tile.Y - FullWallInstep),
new Vector2(tile.X + 0.5f, -tile.Y + FullWallInstep - 1)));
}
}
}
private void AddRectForThinWall(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
{
if (_navMapSystem == null || _grid == null)
return;
var leftTop = new Vector2(-0.5f, -0.5f + ThinWallThickness);
var rightBottom = new Vector2(0.5f, -0.5f);
foreach (var (direction, mask) in tileData)
{
var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
if ((mask & flag) == 0)
continue;
var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
var angle = new Angle(0);
switch (direction)
{
case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f); break;
case AtmosDirection.South: angle = new Angle(MathF.PI); break;
case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
}
TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
}
}
private void AddRectForThinAirlock(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
{
if (_navMapSystem == null || _grid == null)
return;
var leftTop = new Vector2(-0.5f + FullWallInstep, -0.5f + FullWallInstep + ThinDoorThickness);
var rightBottom = new Vector2(0.5f - FullWallInstep, -0.5f + FullWallInstep);
var centreTop = new Vector2(0f, -0.5f + FullWallInstep + ThinDoorThickness);
var centreBottom = new Vector2(0f, -0.5f + FullWallInstep);
foreach (var (direction, mask) in tileData)
{
var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
if ((mask & flag) == 0)
continue;
var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
var angle = new Angle(0);
switch (direction)
{
case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f);break;
case AtmosDirection.South: angle = new Angle(MathF.PI); break;
case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
}
TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
TileLines.Add((angle.RotateVec(centreTop) + tilePosition, angle.RotateVec(centreBottom) + tilePosition));
}
}
protected void AddOrUpdateNavMapLine
(Vector2i origin,
Vector2i terminus,
Dictionary<(int, Vector2i), (int, Vector2i)> lookup,
Dictionary<(int, Vector2i), (int, Vector2i)> lookupReversed,
int index = 0)
{
(int, Vector2i) foundTermiusTuple;
(int, Vector2i) foundOriginTuple;
if (lookup.TryGetValue((index, terminus), out foundTermiusTuple) &&
lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
{
lookup[foundOriginTuple] = foundTermiusTuple;
lookupReversed[foundTermiusTuple] = foundOriginTuple;
lookup.Remove((index, terminus));
lookupReversed.Remove((index, origin));
}
else if (lookup.TryGetValue((index, terminus), out foundTermiusTuple))
{
lookup[(index, origin)] = foundTermiusTuple;
lookup.Remove((index, terminus));
lookupReversed[foundTermiusTuple] = (index, origin);
}
else if (lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
{
lookupReversed[(index, terminus)] = foundOriginTuple;
lookupReversed.Remove(foundOriginTuple);
lookup[foundOriginTuple] = (index, terminus);
}
else
{
lookup.Add((index, origin), (index, terminus));
lookupReversed.Add((index, terminus), (index, origin));
}
}
protected Vector2 GetOffset()
@@ -612,15 +759,3 @@ public struct NavMapBlip
Selectable = selectable;
}
}
public struct NavMapLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public NavMapLine(Vector2 origin, Vector2 terminus)
{
Origin = origin;
Terminus = terminus;
}
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Polymorph.Components;
using Content.Shared.Polymorph.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Polymorph.Systems;
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
}
private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
{
CopyComp<SpriteComponent>(ent);
CopyComp<GenericVisualizerComponent>(ent);
CopyComp<SolutionContainerVisualsComponent>(ent);
// reload appearance to hopefully prevent any invisible layers
if (_appearanceQuery.TryComp(ent, out var appearance))
_appearance.QueueUpdate(ent, appearance);
}
}

View File

@@ -23,8 +23,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
public List<PowerMonitoringConsoleLineGroup> HiddenLineGroups = new();
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? PowerCableNetwork;
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? FocusCableNetwork;
public List<PowerMonitoringConsoleLine> PowerCableNetwork = new();
public List<PowerMonitoringConsoleLine> FocusCableNetwork = new();
private MapGridComponent? _grid;
@@ -48,15 +48,15 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (!_entManager.TryGetComponent<PowerMonitoringCableNetworksComponent>(Owner, out var cableNetworks))
return;
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks);
FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks);
}
public void DrawAllCableNetworks(DrawingHandleScreen handle)
{
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
// Draw full cable network
if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
{
@@ -69,36 +69,29 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
DrawCableNetwork(handle, FocusCableNetwork, Color.White);
}
public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary<Vector2i, List<PowerMonitoringConsoleLine>> fullCableNetwork, Color modulator)
public void DrawCableNetwork(DrawingHandleScreen handle, List<PowerMonitoringConsoleLine> fullCableNetwork, Color modulator)
{
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
var offset = GetOffset();
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
offset = offset with { Y = -offset.Y };
if (WorldRange / WorldMaxRange > 0.5f)
{
var cableNetworks = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
foreach (var line in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
if (HiddenLineGroups.Contains(line.Group))
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
var cableOffset = _powerCableOffsets[(int) line.Group];
var start = ScalePosition(line.Origin + cableOffset - offset);
var end = ScalePosition(line.Terminus + cableOffset - offset);
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
cableNetworks[(int) chunkedLine.Group].Add(start);
cableNetworks[(int) chunkedLine.Group].Add(end);
}
cableNetworks[(int) line.Group].Add(start);
cableNetworks[(int) line.Group].Add(end);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
@@ -124,48 +117,39 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
{
var cableVertexUVs = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
foreach (var line in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
if (HiddenLineGroups.Contains(line.Group))
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
var cableOffset = _powerCableOffsets[(int) line.Group];
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var leftTop = ScalePosition(new Vector2
(Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ cableOffset - offset);
var leftTop = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightTop = ScalePosition(new Vector2
(Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ cableOffset - offset);
var rightTop = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var leftBottom = ScalePosition(new Vector2
(Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ cableOffset - offset);
var leftBottom = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightBottom = ScalePosition(new Vector2
(Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ cableOffset - offset);
var rightBottom = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
}
cableVertexUVs[(int) line.Group].Add(leftBottom);
cableVertexUVs[(int) line.Group].Add(leftTop);
cableVertexUVs[(int) line.Group].Add(rightBottom);
cableVertexUVs[(int) line.Group].Add(leftTop);
cableVertexUVs[(int) line.Group].Add(rightBottom);
cableVertexUVs[(int) line.Group].Add(rightTop);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
@@ -188,23 +172,28 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
}
}
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks, MapGridComponent? grid)
public List<PowerMonitoringConsoleLine> GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks)
{
if (chunks == null || grid == null)
return null;
var decodedOutput = new List<PowerMonitoringConsoleLine>();
var decodedOutput = new Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>();
if (!_entManager.TryGetComponent(MapUid, out _grid))
return decodedOutput;
if (chunks == null)
return decodedOutput;
// We'll use the following dictionaries to combine collinear power cable lines
HorizLinesLookup.Clear();
HorizLinesLookupReversed.Clear();
VertLinesLookup.Clear();
VertLinesLookupReversed.Clear();
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<PowerMonitoringConsoleLine>();
for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
{
var chunkMask = chunk.PowerCableData[cableIdx];
Vector2 offset = _powerCableOffsets[cableIdx];
for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
{
var value = (int) Math.Pow(2, chunkIdx);
@@ -214,8 +203,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
continue;
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
tile = tile with { Y = -tile.Y };
PowerCableChunk neighborChunk;
bool neighbor;
@@ -237,12 +226,7 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(1f, 0f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed, cableIdx);
}
// North
@@ -260,21 +244,21 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(0f, -1f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed, cableIdx);
}
}
}
if (list.Count > 0)
decodedOutput.Add(chunkOrigin, list);
}
var gridOffset = new Vector2(_grid.TileSize * 0.5f, -_grid.TileSize * 0.5f);
foreach (var (origin, terminal) in HorizLinesLookup)
decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
foreach (var (origin, terminal) in VertLinesLookup)
decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
return decodedOutput;
}
}

View File

@@ -170,9 +170,6 @@ public sealed partial class PowerMonitoringWindow : FancyWindow
NavMap.TrackedEntities[mon.Value] = blip;
}
// Update nav map
NavMap.ForceNavMapUpdate();
// If the entry group doesn't match the current tab, the data is out dated, do not use it
if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
return;

View File

@@ -135,6 +135,9 @@ namespace Content.Client.Preferences.UI
_humanoidProfileEditor.CharacterSlot = characterIndexCopy;
_humanoidProfileEditor.UpdateControls();
_preferencesManager.SelectCharacter(character);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(_humanoidProfileEditor.Profile);
controller.ReloadCharacterUI();
UpdateUI();
args.Event.Handle();
};

View File

@@ -1115,6 +1115,7 @@ namespace Content.Client.Preferences.UI
UpdateEyePickers();
UpdateSaveButton();
UpdateLoadouts();
UpdateRoleRequirements();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();

View File

@@ -26,7 +26,7 @@ public sealed class StorageSystem : SharedStorageSystem
SubscribeLocalEvent<StorageComponent, ComponentShutdown>(OnShutdown);
SubscribeNetworkEvent<PickupAnimationEvent>(HandlePickupAnimation);
SubscribeNetworkEvent<AnimateInsertingEntitiesEvent>(HandleAnimatingInsertingEntities);
SubscribeAllEvent<AnimateInsertingEntitiesEvent>(HandleAnimatingInsertingEntities);
}
public override void UpdateUI(Entity<StorageComponent?> entity)

View File

@@ -1234,6 +1234,11 @@ namespace Content.Client.Stylesheets
new StyleProperty("font", notoSans10),
}),
Element<RichTextLabel>()
.Class(StyleClassItemStatus)
.Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)
.Prop(nameof(Control.Margin), new Thickness(0, 0, 0, -6)),
// Slider
new StyleRule(SelectorElement.Type(typeof(Slider)), new []
{

View File

@@ -1,18 +0,0 @@
using Content.Client.Tools.UI;
using Content.Shared.Tools.Components;
namespace Content.Client.Tools.Components
{
[RegisterComponent, Access(typeof(ToolSystem), typeof(WelderStatusControl))]
public sealed partial class WelderComponent : SharedWelderComponent
{
[ViewVariables(VVAccess.ReadWrite)]
public bool UiUpdateNeeded { get; set; }
[ViewVariables]
public float FuelCapacity { get; set; }
[ViewVariables]
public float Fuel { get; set; }
}
}

View File

@@ -1,10 +1,8 @@
using Content.Client.Items;
using Content.Client.Tools.Components;
using Content.Client.Tools.UI;
using Content.Shared.Item;
using Content.Shared.Tools.Components;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
namespace Content.Client.Tools
@@ -15,8 +13,7 @@ namespace Content.Client.Tools
{
base.Initialize();
SubscribeLocalEvent<WelderComponent, ComponentHandleState>(OnWelderHandleState);
Subs.ItemStatus<WelderComponent>(ent => new WelderStatusControl(ent));
Subs.ItemStatus<WelderComponent>(ent => new WelderStatusControl(ent, EntityManager, this));
Subs.ItemStatus<MultipleToolComponent>(ent => new MultipleToolStatusControl(ent));
}
@@ -42,20 +39,5 @@ namespace Content.Client.Tools
sprite.LayerSetSprite(0, current.Sprite);
}
}
private void OnWelderHandleState(EntityUid uid, WelderComponent welder, ref ComponentHandleState args)
{
if (args.Current is not WelderComponentState state)
return;
welder.FuelCapacity = state.FuelCapacity;
welder.Fuel = state.Fuel;
welder.UiUpdateNeeded = true;
}
protected override bool IsWelder(EntityUid uid)
{
return HasComp<WelderComponent>(uid);
}
}
}

View File

@@ -1,62 +1,45 @@
using Content.Client.Items.UI;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Client.Tools.Components;
using Content.Shared.Item;
using Robust.Client.UserInterface;
using Content.Shared.FixedPoint;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
namespace Content.Client.Tools.UI;
public sealed class WelderStatusControl : Control
public sealed class WelderStatusControl : PollingItemStatusControl<WelderStatusControl.Data>
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly WelderComponent _parent;
private readonly ItemToggleComponent? _toggleComponent;
private readonly Entity<WelderComponent> _parent;
private readonly IEntityManager _entityManager;
private readonly SharedToolSystem _toolSystem;
private readonly RichTextLabel _label;
public WelderStatusControl(Entity<WelderComponent> parent)
public WelderStatusControl(Entity<WelderComponent> parent, IEntityManager entityManager, SharedToolSystem toolSystem)
{
_parent = parent;
_entMan = IoCManager.Resolve<IEntityManager>();
if (_entMan.TryGetComponent<ItemToggleComponent>(parent, out var itemToggle))
_toggleComponent = itemToggle;
_entityManager = entityManager;
_toolSystem = toolSystem;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
UpdateDraw();
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
protected override Data PollData()
{
base.FrameUpdate(args);
if (!_parent.UiUpdateNeeded)
{
return;
}
Update();
var (fuel, capacity) = _toolSystem.GetWelderFuelAndCapacity(_parent, _parent.Comp);
return new Data(fuel, capacity, _parent.Comp.Enabled);
}
public void Update()
protected override void Update(in Data data)
{
_parent.UiUpdateNeeded = false;
var fuelCap = _parent.FuelCapacity;
var fuel = _parent.Fuel;
var lit = false;
if (_toggleComponent != null)
{
lit = _toggleComponent.Activated;
}
_label.SetMarkup(Loc.GetString("welder-component-on-examine-detailed-message",
("colorName", fuel < fuelCap / 4f ? "darkorange" : "orange"),
("fuelLeft", Math.Round(fuel, 1)),
("fuelCapacity", fuelCap),
("status", Loc.GetString(lit ? "welder-component-on-examine-welder-lit-message" : "welder-component-on-examine-welder-not-lit-message"))));
("colorName", data.Fuel < data.FuelCapacity / 4f ? "darkorange" : "orange"),
("fuelLeft", data.Fuel),
("fuelCapacity", data.FuelCapacity),
("status", Loc.GetString(data.Lit ? "welder-component-on-examine-welder-lit-message" : "welder-component-on-examine-welder-not-lit-message"))));
}
public record struct Data(FixedPoint2 Fuel, FixedPoint2 FuelCapacity, bool Lit);
}

View File

@@ -51,6 +51,7 @@ namespace Content.Client.UserInterface.Controls
var stretch = _cfg.GetCVar(CCVars.ViewportStretch);
var renderScaleUp = _cfg.GetCVar(CCVars.ViewportScaleRender);
var fixedFactor = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var verticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit);
if (stretch)
{
@@ -60,6 +61,7 @@ namespace Content.Client.UserInterface.Controls
// Did not find a snap, enable stretching.
Viewport.FixedStretchSize = null;
Viewport.StretchMode = ScalingViewportStretchMode.Bilinear;
Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;
if (renderScaleUp)
{
@@ -104,6 +106,8 @@ namespace Content.Client.UserInterface.Controls
// where we are clipping the viewport to make it fit.
var cfgToleranceClip = _cfg.GetCVar(CCVars.ViewportSnapToleranceClip);
var cfgVerticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit);
// Calculate if the viewport, when rendered at an integer scale,
// is close enough to the control size to enable "snapping" to NN,
// potentially cutting a tiny bit off/leaving a margin.
@@ -123,7 +127,8 @@ namespace Content.Client.UserInterface.Controls
// The rule for which snap fits is that at LEAST one axis needs to be in the tolerance size wise.
// One axis MAY be larger but not smaller than tolerance.
// Obviously if it's too small it's bad, and if it's too big on both axis we should stretch up.
if (Fits(dx) && Fits(dy) || Fits(dx) && Larger(dy) || Larger(dx) && Fits(dy))
// Additionally, if the viewport's supposed to be vertically fit, then the horizontal scale should just be ignored where appropriate.
if ((Fits(dx) || cfgVerticalFit) && Fits(dy) || !cfgVerticalFit && Fits(dx) && Larger(dy) || Larger(dx) && Fits(dy))
{
// Found snap that fits.
return i;

View File

@@ -9,6 +9,7 @@ namespace Content.Client.UserInterface.Controls
public SlotButton(SlotData slotData)
{
ButtonTexturePath = slotData.TextureName;
FullButtonTexturePath = slotData.FullTextureName;
Blocked = slotData.Blocked;
Highlight = slotData.Highlighted;
StorageTexturePath = "Slots/back";

View File

@@ -15,11 +15,12 @@ namespace Content.Client.UserInterface.Controls
public TextureRect ButtonRect { get; }
public TextureRect BlockedRect { get; }
public TextureRect HighlightRect { get; }
public SpriteView SpriteView { get; }
public SpriteView HoverSpriteView { get; }
public TextureButton StorageButton { get; }
public CooldownGraphic CooldownDisplay { get; }
private SpriteView SpriteView { get; }
public EntityUid? Entity => SpriteView.Entity;
private bool _slotNameSet;
@@ -68,7 +69,18 @@ namespace Content.Client.UserInterface.Controls
set
{
_buttonTexturePath = value;
ButtonRect.Texture = Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
UpdateButtonTexture();
}
}
private string? _fullButtonTexturePath;
public string? FullButtonTexturePath
{
get => _fullButtonTexturePath;
set
{
_fullButtonTexturePath = value;
UpdateButtonTexture();
}
}
@@ -197,6 +209,21 @@ namespace Content.Client.UserInterface.Controls
HoverSpriteView.SetEntity(null);
}
public void SetEntity(EntityUid? ent)
{
SpriteView.SetEntity(ent);
UpdateButtonTexture();
}
private void UpdateButtonTexture()
{
var fullTexture = Theme.ResolveTextureOrNull(_fullButtonTexturePath);
var texture = Entity.HasValue && fullTexture != null
? fullTexture.Texture
: Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
ButtonRect.Texture = texture;
}
private void OnButtonPressed(GUIBoundKeyEventArgs args)
{
Pressed?.Invoke(args, this);
@@ -229,8 +256,8 @@ namespace Content.Client.UserInterface.Controls
base.OnThemeUpdated();
StorageButton.TextureNormal = Theme.ResolveTextureOrNull(_storageTexturePath)?.Texture;
ButtonRect.Texture = Theme.ResolveTextureOrNull(_buttonTexturePath)?.Texture;
HighlightRect.Texture = Theme.ResolveTextureOrNull(_highlightTexturePath)?.Texture;
UpdateButtonTexture();
}
EntityUid? IEntityControl.UiEntity => Entity;

View File

@@ -28,6 +28,15 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private readonly Dictionary<string, HandButton> _handLookup = new();
private HandsComponent? _playerHandsComponent;
private HandButton? _activeHand = null;
// We only have two item status controls (left and right hand),
// but we may have more than two hands.
// We handle this by having the item status be the *last active* hand of that side.
// These variables store which that is.
// ("middle" hands are hardcoded as right, whatever)
private HandButton? _statusHandLeft;
private HandButton? _statusHandRight;
private int _backupSuffix = 0; //this is used when autogenerating container names if they don't have names
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
@@ -120,12 +129,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (_entities.TryGetComponent(hand.HeldEntity, out VirtualItemComponent? virt))
{
handButton.SpriteView.SetEntity(virt.BlockingEntity);
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
else
{
handButton.SpriteView.SetEntity(hand.HeldEntity);
handButton.SetEntity(hand.HeldEntity);
handButton.Blocked = false;
}
}
@@ -171,17 +180,16 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (_entities.TryGetComponent(entity, out VirtualItemComponent? virt))
{
hand.SpriteView.SetEntity(virt.BlockingEntity);
hand.SetEntity(virt.BlockingEntity);
hand.Blocked = true;
}
else
{
hand.SpriteView.SetEntity(entity);
hand.SetEntity(entity);
hand.Blocked = false;
}
if (_playerHandsComponent?.ActiveHand?.Name == name)
HandsGui?.UpdatePanelEntity(entity);
UpdateHandStatus(hand, entity);
}
private void OnItemRemoved(string name, EntityUid entity)
@@ -190,9 +198,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (hand == null)
return;
hand.SpriteView.SetEntity(null);
if (_playerHandsComponent?.ActiveHand?.Name == name)
HandsGui?.UpdatePanelEntity(null);
hand.SetEntity(null);
UpdateHandStatus(hand, null);
}
private HandsContainer GetFirstAvailableContainer()
@@ -232,7 +239,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (_activeHand != null)
_activeHand.Highlight = false;
HandsGui?.UpdatePanelEntity(null);
return;
}
@@ -250,7 +256,19 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
{
HandsGui.UpdatePanelEntity(hand.HeldEntity);
if (hand.Location == HandLocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
}
else
{
// Middle or right
_statusHandRight = handControl;
HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
}
HandsGui.SetHighlightHand(hand.Location);
}
}
@@ -278,6 +296,14 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
GetFirstAvailableContainer().AddButton(button);
}
// If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time.
if (location == HandLocation.Left)
_statusHandLeft ??= button;
else
_statusHandRight ??= button;
return button;
}
@@ -336,6 +362,11 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
handContainer.RemoveButton(handButton);
}
if (_statusHandLeft == handButton)
_statusHandLeft = null;
if (_statusHandRight == handButton)
_statusHandRight = null;
_handLookup.Remove(handName);
handButton.Dispose();
return true;
@@ -407,4 +438,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
}
}
}
private void UpdateHandStatus(HandButton hand, EntityUid? entity)
{
if (hand == _statusHandLeft)
HandsGui?.UpdatePanelEntityLeft(entity);
if (hand == _statusHandRight)
HandsGui?.UpdatePanelEntityRight(entity);
}
}

View File

@@ -31,7 +31,7 @@ public sealed class HotbarUIController : UIController
ReloadHotbar();
}
public void Setup(HandsContainer handsContainer, ItemStatusPanel handStatus, StorageContainer storageContainer)
public void Setup(HandsContainer handsContainer, StorageContainer storageContainer)
{
_inventory = UIManager.GetUIController<InventoryUIController>();
_hands = UIManager.GetUIController<HandsUIController>();

View File

@@ -10,21 +10,14 @@
Orientation="Vertical"
HorizontalAlignment="Center">
<BoxContainer Orientation="Vertical">
<Control HorizontalAlignment="Center">
<inventory:ItemStatusPanel
Name="StatusPanel"
Visible="False"
HorizontalAlignment="Center"
/>
<BoxContainer Name="StorageContainer"
Access="Public"
HorizontalAlignment="Center"
Margin="10">
<storage:StorageContainer
Name="StoragePanel"
Visible="False"/>
</BoxContainer>
</Control>
<BoxContainer Name="StorageContainer"
Access="Public"
HorizontalAlignment="Center"
Margin="10">
<storage:StorageContainer
Name="StoragePanel"
Visible="False"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Name="Hotbar" HorizontalAlignment="Center">
<inventory:ItemSlotButtonContainer
Name="SecondHotbar"
@@ -35,13 +28,22 @@
ExpandBackwards="True"
Columns="6"
HorizontalExpand="False"/>
<inventory:ItemStatusPanel
Name="StatusPanelRight"
HorizontalAlignment="Center" Margin="0 0 -2 2"
SetWidth="125"
MaxHeight="60"/>
<hands:HandsContainer
Name="HandContainer"
Access="Public"
HorizontalAlignment="Center"
HorizontalExpand="False"
ColumnLimit="6"
Margin="4 0 4 0"/>
ColumnLimit="6"/>
<inventory:ItemStatusPanel
Name="StatusPanelLeft"
HorizontalAlignment="Center" Margin="-2 0 0 2"
SetWidth="125"
MaxHeight="60"/>
<inventory:ItemSlotButtonContainer
Name="MainHotbar"
SlotGroup="MainHotbar"

View File

@@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Content.Shared.Hands.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -10,22 +11,27 @@ public sealed partial class HotbarGui : UIWidget
public HotbarGui()
{
RobustXamlLoader.Load(this);
StatusPanel.Update(null);
StatusPanelRight.SetSide(HandLocation.Right);
StatusPanelLeft.SetSide(HandLocation.Left);
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
hotbarController.Setup(HandContainer, StatusPanel, StoragePanel);
hotbarController.Setup(HandContainer, StoragePanel);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
public void UpdatePanelEntity(EntityUid? entity)
public void UpdatePanelEntityLeft(EntityUid? entity)
{
StatusPanel.Update(entity);
if (entity == null)
{
StatusPanel.Visible = false;
return;
}
StatusPanelLeft.Update(entity);
}
StatusPanel.Visible = true;
public void UpdatePanelEntityRight(EntityUid? entity)
{
StatusPanelRight.Update(entity);
}
public void SetHighlightHand(HandLocation? hand)
{
StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left);
StatusPanelRight.UpdateHighlight(hand is HandLocation.Middle or HandLocation.Right);
}
}

View File

@@ -3,22 +3,26 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
MinSize="150 0">
<PanelContainer
Name="Panel"
ModulateSelfOverride="#FFFFFFE6"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture
ContentMarginLeftOverride="6"
ContentMarginRightOverride="6"
ContentMarginTopOverride="4"
ContentMarginBottomOverride="4" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" SeparationOverride="0">
<BoxContainer Name="StatusContents" Orientation="Vertical"/>
<Label Name="ItemNameLabel" StyleClasses="ItemStatus"/>
HorizontalAlignment="Center">
<Control Name="VisWrapper" Visible="False">
<PanelContainer Name="Panel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture
PatchMarginBottom="4"
PatchMarginTop="6"
TextureScale="2 2"
Mode="Tile"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer Name="HighlightPanel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture PatchMarginBottom="4" PatchMarginTop="6" TextureScale="2 2">
</graphics:StyleBoxTexture>
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4">
<BoxContainer Name="StatusContents" Orientation="Vertical" />
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus" Align="Left" />
</BoxContainer>
</PanelContainer>
</Control>
</controls:ItemStatusPanel>

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Client.Items;
using Content.Client.Resources;
using Content.Shared.Hands.Components;
@@ -5,6 +6,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Inventory.VirtualItem;
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;
@@ -14,12 +16,15 @@ using static Content.Client.IoC.StaticIoC;
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
[GenerateTypedNameReferences]
public sealed partial class ItemStatusPanel : BoxContainer
public sealed partial class ItemStatusPanel : Control
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private EntityUid? _entity;
// Tracked so we can re-run SetSide() if the theme changes.
private HandLocation _side;
public ItemStatusPanel()
{
RobustXamlLoader.Load(this);
@@ -30,41 +35,65 @@ public sealed partial class ItemStatusPanel : BoxContainer
public void SetSide(HandLocation location)
{
string texture;
// AN IMPORTANT REMINDER ABOUT THIS CODE:
// In the UI, the RIGHT hand is on the LEFT on the screen.
// So that a character facing DOWN matches the hand positions.
Texture? texture;
Texture? textureHighlight;
StyleBox.Margin cutOut;
StyleBox.Margin flat;
Label.AlignMode textAlign;
Thickness contentMargin;
switch (location)
{
case HandLocation.Left:
texture = "/Textures/Interface/Nano/item_status_right.svg.96dpi.png";
cutOut = StyleBox.Margin.Left | StyleBox.Margin.Top;
flat = StyleBox.Margin.Right | StyleBox.Margin.Bottom;
textAlign = Label.AlignMode.Right;
case HandLocation.Right:
texture = Theme.ResolveTexture("item_status_right");
textureHighlight = Theme.ResolveTexture("item_status_right_highlight");
cutOut = StyleBox.Margin.Left;
flat = StyleBox.Margin.Right;
contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right");
break;
case HandLocation.Middle:
texture = "/Textures/Interface/Nano/item_status_middle.svg.96dpi.png";
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
textAlign = Label.AlignMode.Left;
break;
case HandLocation.Right:
texture = "/Textures/Interface/Nano/item_status_left.svg.96dpi.png";
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
textAlign = Label.AlignMode.Left;
case HandLocation.Left:
texture = Theme.ResolveTexture("item_status_left");
textureHighlight = Theme.ResolveTexture("item_status_left_highlight");
cutOut = StyleBox.Margin.Right;
flat = StyleBox.Margin.Left;
contentMargin = MarginFromThemeColor("_itemstatus_content_margin_left");
break;
default:
throw new ArgumentOutOfRangeException(nameof(location), location, null);
}
var panel = (StyleBoxTexture) Panel.PanelOverride!;
panel.Texture = ResC.GetTexture(texture);
panel.SetPatchMargin(flat, 2);
panel.SetPatchMargin(cutOut, 13);
Contents.Margin = contentMargin;
ItemNameLabel.Align = textAlign;
var panel = (StyleBoxTexture) Panel.PanelOverride!;
panel.Texture = texture;
panel.SetPatchMargin(flat, 4);
panel.SetPatchMargin(cutOut, 7);
var panelHighlight = (StyleBoxTexture) HighlightPanel.PanelOverride!;
panelHighlight.Texture = textureHighlight;
panelHighlight.SetPatchMargin(flat, 4);
panelHighlight.SetPatchMargin(cutOut, 7);
_side = location;
}
private Thickness MarginFromThemeColor(string itemName)
{
// This is the worst thing I've ever programmed
// (can you tell I'm a graphics programmer?)
// (the margin needs to change depending on the UI theme, so we use a fake color entry to store the value)
var color = Theme.ResolveColorOrSpecified(itemName);
return new Thickness(color.RByte, color.GByte, color.BByte, color.AByte);
}
protected override void OnThemeUpdated()
{
SetSide(_side);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -79,7 +108,7 @@ public sealed partial class ItemStatusPanel : BoxContainer
{
ClearOldStatus();
_entity = null;
Panel.Visible = false;
VisWrapper.Visible = false;
return;
}
@@ -91,7 +120,12 @@ public sealed partial class ItemStatusPanel : BoxContainer
UpdateItemName();
}
Panel.Visible = true;
VisWrapper.Visible = true;
}
public void UpdateHighlight(bool highlight)
{
HighlightPanel.Visible = highlight;
}
private void UpdateItemName()

View File

@@ -417,7 +417,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
if (_strippingWindow?.InventoryButtons.GetButton(update.Name) is { } inventoryButton)
{
inventoryButton.SpriteView.SetEntity(entity);
inventoryButton.SetEntity(entity);
inventoryButton.StorageButton.Visible = showStorage;
}
@@ -426,12 +426,12 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
if (_entities.TryGetComponent(entity, out VirtualItemComponent? virtb))
{
button.SpriteView.SetEntity(virtb.BlockingEntity);
button.SetEntity(virtb.BlockingEntity);
button.Blocked = true;
}
else
{
button.SpriteView.SetEntity(entity);
button.SetEntity(entity);
button.Blocked = false;
button.StorageButton.Visible = showStorage;
}

View File

@@ -25,6 +25,7 @@ public sealed class ViewportUIController : UIController
_configurationManager.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportRatio());
_configurationManager.OnValueChanged(CCVars.ViewportMaximumWidth, _ => UpdateViewportRatio());
_configurationManager.OnValueChanged(CCVars.ViewportWidth, _ => UpdateViewportRatio());
_configurationManager.OnValueChanged(CCVars.ViewportVerticalFit, _ => UpdateViewportRatio());
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
@@ -45,13 +46,19 @@ public sealed class ViewportUIController : UIController
var min = _configurationManager.GetCVar(CCVars.ViewportMinimumWidth);
var max = _configurationManager.GetCVar(CCVars.ViewportMaximumWidth);
var width = _configurationManager.GetCVar(CCVars.ViewportWidth);
var verticalfit = _configurationManager.GetCVar(CCVars.ViewportVerticalFit) && _configurationManager.GetCVar(CCVars.ViewportStretch);
if (width < min || width > max)
if (verticalfit)
{
width = max;
}
else if (width < min || width > max)
{
width = CCVars.ViewportWidth.DefaultValue;
}
Viewport.Viewport.ViewportSize = (EyeManager.PixelsPerMeter * width, EyeManager.PixelsPerMeter * ViewportHeight);
Viewport.UpdateCfg();
}
public void ReloadViewport()

View File

@@ -32,6 +32,7 @@ namespace Content.Client.Viewport
private int _curRenderScale;
private ScalingViewportStretchMode _stretchMode = ScalingViewportStretchMode.Bilinear;
private ScalingViewportRenderScaleMode _renderScaleMode = ScalingViewportRenderScaleMode.Fixed;
private ScalingViewportIgnoreDimension _ignoreDimension = ScalingViewportIgnoreDimension.None;
private int _fixedRenderScale = 1;
private readonly List<CopyPixelsDelegate<Rgba32>> _queuedScreenshots = new();
@@ -106,6 +107,17 @@ namespace Content.Client.Viewport
}
}
[ViewVariables(VVAccess.ReadWrite)]
public ScalingViewportIgnoreDimension IgnoreDimension
{
get => _ignoreDimension;
set
{
_ignoreDimension = value;
InvalidateViewport();
}
}
public ScalingViewport()
{
IoCManager.InjectDependencies(this);
@@ -178,7 +190,19 @@ namespace Content.Client.Viewport
if (FixedStretchSize == null)
{
var (ratioX, ratioY) = ourSize / vpSize;
var ratio = Math.Min(ratioX, ratioY);
var ratio = 1f;
switch (_ignoreDimension)
{
case ScalingViewportIgnoreDimension.None:
ratio = Math.Min(ratioX, ratioY);
break;
case ScalingViewportIgnoreDimension.Vertical:
ratio = ratioX;
break;
case ScalingViewportIgnoreDimension.Horizontal:
ratio = ratioY;
break;
}
var size = vpSize * ratio;
// Size
@@ -357,4 +381,25 @@ namespace Content.Client.Viewport
/// </summary>
CeilInt
}
/// <summary>
/// If the viewport is allowed to freely scale, this determines which dimensions should be ignored while fitting the viewport
/// </summary>
public enum ScalingViewportIgnoreDimension
{
/// <summary>
/// The viewport won't ignore any dimension.
/// </summary>
None = 0,
/// <summary>
/// The viewport will ignore the horizontal dimension, and will exclusively consider the vertical dimension for scaling.
/// </summary>
Horizontal,
/// <summary>
/// The viewport will ignore the vertical dimension, and will exclusively consider the horizontal dimension for scaling.
/// </summary>
Vertical
}
}

View File

@@ -0,0 +1,178 @@
using System.Numerics;
using Content.Client.Resources;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
namespace Content.Client.Weapons.Ranged.ItemStatus;
/// <summary>
/// Renders one or more rows of bullets for item status.
/// </summary>
/// <remarks>
/// This is a custom control to allow complex responsive layout logic.
/// </remarks>
public sealed class BulletRender : Control
{
private static readonly Color ColorA = Color.FromHex("#b68f0e");
private static readonly Color ColorB = Color.FromHex("#d7df60");
private static readonly Color ColorGoneA = Color.FromHex("#000000");
private static readonly Color ColorGoneB = Color.FromHex("#222222");
/// <summary>
/// Try to ensure there's at least this many bullets on one row.
/// </summary>
/// <remarks>
/// For example, if there are two rows and the second row has only two bullets,
/// we "steal" some bullets from the row below it to make it look nicer.
/// </remarks>
public const int MinCountPerRow = 7;
public const int BulletHeight = 12;
public const int BulletSeparationNormal = 3;
public const int BulletSeparationTiny = 2;
public const int BulletWidthNormal = 5;
public const int BulletWidthTiny = 2;
public const int VerticalSeparation = 2;
private readonly Texture _bulletTiny;
private readonly Texture _bulletNormal;
private int _capacity;
private BulletType _type = BulletType.Normal;
public int Rows { get; set; } = 2;
public int Count { get; set; }
public int Capacity
{
get => _capacity;
set
{
_capacity = value;
InvalidateMeasure();
}
}
public BulletType Type
{
get => _type;
set
{
_type = value;
InvalidateMeasure();
}
}
public BulletRender()
{
var resC = IoCManager.Resolve<IResourceCache>();
_bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png");
_bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png");
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
var height = BulletHeight * rows + (BulletSeparationNormal * rows - 1);
var width = RowWidth(countPerRow);
return new Vector2(width, height);
}
protected override void Draw(DrawingHandleScreen handle)
{
// Scale rendering in this control by UIScale.
var currentTransform = handle.GetTransform();
handle.SetTransform(Matrix3.CreateScale(new Vector2(UIScale)) * currentTransform);
var countPerRow = CountPerRow(Size.X);
var (separation, _) = BulletParams();
var texture = Type == BulletType.Normal ? _bulletNormal : _bulletTiny;
var pos = new Vector2();
var altColor = false;
var spent = Capacity - Count;
var bulletsDone = 0;
// Draw by rows, bottom to top.
for (var row = 0; row < Rows; row++)
{
altColor = false;
var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone);
if (thisRowCount <= 0)
break;
// Handle MinCountPerRow
// We only do this if:
// 1. The next row would have less than MinCountPerRow bullets.
// 2. The next row is actually visible (we aren't the last row).
// 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases).
var nextRowCount = Capacity - bulletsDone - thisRowCount;
if (nextRowCount < MinCountPerRow && row != Rows - 1 && MinCountPerRow < countPerRow)
thisRowCount -= MinCountPerRow - nextRowCount;
// Account for row width to right-align.
var rowWidth = RowWidth(thisRowCount);
pos.X += Size.X - rowWidth;
// Draw row left to right (so overlapping works)
for (var bullet = 0; bullet < thisRowCount; bullet++)
{
var absIdx = Capacity - bulletsDone - thisRowCount + bullet;
Color color;
if (absIdx >= spent)
color = altColor ? ColorA : ColorB;
else
color = altColor ? ColorGoneA : ColorGoneB;
var renderPos = pos;
renderPos.Y = Size.Y - renderPos.Y - BulletHeight;
handle.DrawTexture(texture, renderPos, color);
pos.X += separation;
altColor ^= true;
}
bulletsDone += thisRowCount;
pos.X = 0;
pos.Y += BulletHeight + VerticalSeparation;
}
}
private int CountPerRow(float width)
{
var (separation, bulletWidth) = BulletParams();
return (int) ((width - bulletWidth + separation) / separation);
}
private (int separation, int width) BulletParams()
{
return Type switch
{
BulletType.Normal => (BulletSeparationNormal, BulletWidthNormal),
BulletType.Tiny => (BulletSeparationTiny, BulletWidthTiny),
_ => throw new ArgumentOutOfRangeException()
};
}
private int RowWidth(int count)
{
var (separation, bulletWidth) = BulletParams();
return (count - 1) * separation + bulletWidth;
}
public enum BulletType
{
Normal,
Tiny
}
}

View File

@@ -4,11 +4,11 @@ using Content.Client.Items;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Weapons.Ranged.Components;
using Content.Client.Weapons.Ranged.ItemStatus;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Graphics;
namespace Content.Client.Weapons.Ranged.Systems;
@@ -91,116 +91,26 @@ public sealed partial class GunSystem
private sealed class DefaultStatusControl : Control
{
private readonly BoxContainer _bulletsListTop;
private readonly BoxContainer _bulletsListBottom;
private readonly BulletRender _bulletRender;
public DefaultStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = Control.VAlignment.Center;
AddChild(new BoxContainer
VerticalAlignment = VAlignment.Center;
AddChild(_bulletRender = new BulletRender
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0,
Children =
{
(_bulletsListTop = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 0
}),
new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsListBottom = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
}
},
}
}
}
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
});
}
public void Update(int count, int capacity)
{
_bulletsListTop.RemoveAllChildren();
_bulletsListBottom.RemoveAllChildren();
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
string texturePath;
if (capacity <= 20)
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
}
else if (capacity <= 30)
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
}
else
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
}
var texture = StaticIoC.ResC.GetTexture(texturePath);
const int tinyMaxRow = 60;
if (capacity > tinyMaxRow)
{
FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
}
else
{
FillBulletRow(_bulletsListBottom, count, capacity, texture);
}
}
private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
{
var colorA = Color.FromHex("#b68f0e");
var colorB = Color.FromHex("#d7df60");
var colorGoneA = Color.FromHex("#000000");
var colorGoneB = Color.FromHex("#222222");
var altColor = false;
for (var i = count; i < capacity; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
});
altColor ^= true;
}
for (var i = 0; i < count; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorA : colorB
});
altColor ^= true;
}
_bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
}
}
@@ -291,7 +201,7 @@ public sealed partial class GunSystem
private sealed class ChamberMagazineStatusControl : Control
{
private readonly BoxContainer _bulletsList;
private readonly BulletRender _bulletRender;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
@@ -308,23 +218,16 @@ public sealed partial class GunSystem
HorizontalExpand = true,
Children =
{
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Right,
}),
new Control() { MinSize = new Vector2(5,0) },
new Control
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 5, 0),
Children =
{
(_bulletsList = new BoxContainer
(_bulletRender = new BulletRender
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
}),
(_noMagazineLabel = new Label
{
@@ -333,12 +236,25 @@ public sealed partial class GunSystem
})
}
},
new Control() { MinSize = new Vector2(5,0) },
(_ammoCount = new Label
new BoxContainer
{
StyleClasses = {StyleNano.StyleClassItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
Orientation = BoxContainer.LayoutOrientation.Vertical,
VerticalAlignment = VAlignment.Bottom,
Margin = new Thickness(0, 0, 0, 2),
Children =
{
(_ammoCount = new Label
{
StyleClasses = {StyleNano.StyleClassItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
HorizontalAlignment = HAlignment.Left,
}),
}
}
}
});
}
@@ -348,61 +264,24 @@ public sealed partial class GunSystem
_chamberedBullet.ModulateSelfOverride =
chambered ? Color.FromHex("#d7df60") : Color.Black;
_bulletsList.RemoveAllChildren();
if (!magazine)
{
_bulletRender.Visible = false;
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
_bulletRender.Visible = true;
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
var texture = StaticIoC.ResC.GetTexture(texturePath);
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
_bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
_ammoCount.Text = $"x{count:00}";
capacity = Math.Min(capacity, 20);
FillBulletRow(_bulletsList, count, capacity, texture);
}
private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
{
var colorA = Color.FromHex("#b68f0e");
var colorB = Color.FromHex("#d7df60");
var colorGoneA = Color.FromHex("#000000");
var colorGoneB = Color.FromHex("#222222");
var altColor = false;
// Draw the empty ones
for (var i = count; i < capacity; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
// Draw the full ones, but limit the count to the capacity
count = Math.Min(count, capacity);
for (var i = 0; i < count; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorA : colorB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
}
public void PlayAlarmAnimation(Animation animation)

View File

@@ -39,6 +39,14 @@ public sealed class AnalysisConsoleBoundUserInterface : BoundUserInterface
{
SendMessage(new AnalysisConsoleExtractButtonPressedMessage());
};
_consoleMenu.OnUpBiasButtonPressed += () =>
{
SendMessage(new AnalysisConsoleBiasButtonPressedMessage(false));
};
_consoleMenu.OnDownBiasButtonPressed += () =>
{
SendMessage(new AnalysisConsoleBiasButtonPressedMessage(true));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -47,7 +55,7 @@ public sealed class AnalysisConsoleBoundUserInterface : BoundUserInterface
switch (state)
{
case AnalysisConsoleScanUpdateState msg:
case AnalysisConsoleUpdateState msg:
_consoleMenu?.SetButtonsDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);

View File

@@ -1,30 +1,46 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'analysis-console-menu-title'}"
MinSize="620 280"
SetSize="620 280">
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'analysis-console-menu-title'}"
MinSize="620 280"
SetSize="620 280">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Margin="10 10 10 10" MinWidth="150" Orientation="Vertical" VerticalExpand="True" SizeFlagsStretchRatio="1">
<BoxContainer Margin="10 10 10 10" MinWidth="150" Orientation="Vertical"
VerticalExpand="True" SizeFlagsStretchRatio="1">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<Button Name="ServerSelectionButton"
Text="{Loc 'analysis-console-server-list-button'}"></Button>
Text="{Loc 'analysis-console-server-list-button'}"></Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="ScanButton"
Text="{Loc 'analysis-console-scan-button'}"
ToolTip="{Loc 'analysis-console-scan-tooltip-info'}">
Text="{Loc 'analysis-console-scan-button'}"
ToolTip="{Loc 'analysis-console-scan-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="PrintButton"
Text="{Loc 'analysis-console-print-button'}"
ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
Text="{Loc 'analysis-console-print-button'}"
ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="ExtractButton"
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="UpBiasButton"
Text="{Loc 'analysis-console-bias-up'}"
ToolTip="{Loc 'analysis-console-bias-button-info-up'}"
HorizontalExpand="True"
StyleClasses="OpenRight">
</Button>
<Button Name="DownBiasButton"
Text="{Loc 'analysis-console-bias-down'}"
ToolTip="{Loc 'analysis-console-bias-button-info-down'}"
HorizontalExpand="True"
StyleClasses="OpenLeft">
</Button>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical">
<Label Name="ProgressLabel"></Label>
@@ -36,13 +52,13 @@
</ProgressBar>
</BoxContainer>
</BoxContainer>
<customControls:VSeparator StyleClasses="LowDivider"/>
<customControls:VSeparator StyleClasses="LowDivider" />
<PanelContainer Margin="10 10 10 10" HorizontalExpand="True" SizeFlagsStretchRatio="3">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" >
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer VerticalExpand="True">
<RichTextLabel Name="Information"> </RichTextLabel>
</BoxContainer>

View File

@@ -3,6 +3,7 @@ using Content.Client.UserInterface.Controls;
using Content.Shared.Xenoarchaeology.Equipment;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -19,6 +20,8 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
public event Action? OnScanButtonPressed;
public event Action? OnPrintButtonPressed;
public event Action? OnExtractButtonPressed;
public event Action? OnUpBiasButtonPressed;
public event Action? OnDownBiasButtonPressed;
// For rendering the progress bar, updated from BUI state
private TimeSpan? _startTime;
@@ -36,6 +39,12 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
ExtractButton.OnPressed += _ => OnExtractButtonPressed?.Invoke();
UpBiasButton.OnPressed += _ => OnUpBiasButtonPressed?.Invoke();
DownBiasButton.OnPressed += _ => OnDownBiasButtonPressed?.Invoke();
var buttonGroup = new ButtonGroup(false);
UpBiasButton.Group = buttonGroup;
DownBiasButton.Group = buttonGroup;
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -60,7 +69,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ProgressBar.Value = Math.Clamp(1.0f - (float) remaining.Divide(total), 0.0f, 1.0f);
}
public void SetButtonsDisabled(AnalysisConsoleScanUpdateState state)
public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
{
ScanButton.Disabled = !state.CanScan;
PrintButton.Disabled = !state.CanPrint;
@@ -78,7 +87,6 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ExtractButton.AddStyleClass("ButtonColorGreen");
}
}
private void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
@@ -91,7 +99,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ArtifactDisplay.SetEntity(uid);
}
public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
{
var message = new FormattedMessage();
@@ -129,7 +137,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
Information.SetMessage(message);
}
public void UpdateProgressBar(AnalysisConsoleScanUpdateState state)
public void UpdateProgressBar(AnalysisConsoleUpdateState state)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;

View File

@@ -10,9 +10,8 @@ namespace Content.IntegrationTests.Pair;
public sealed class TestMapData
{
public EntityUid MapUid { get; set; }
public EntityUid GridUid { get; set; }
public MapId MapId { get; set; }
public MapGridComponent MapGrid { get; set; } = default!;
public Entity<MapGridComponent> Grid;
public MapId MapId;
public EntityCoordinates GridCoords { get; set; }
public MapCoordinates MapCoords { get; set; }
public TileRef Tile { get; set; }
@@ -21,4 +20,4 @@ public sealed class TestMapData
public EntityUid CMapUid { get; set; }
public EntityUid CGridUid { get; set; }
public EntityCoordinates CGridCoords { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -14,36 +15,37 @@ public sealed partial class TestPair
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// </summary>
public async Task<TestMapData> CreateTestMap()
[MemberNotNull(nameof(TestMap))]
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
{
var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitPost(() =>
{
mapData.MapId = Server.MapMan.CreateMap();
mapData.MapUid = Server.MapMan.GetMapEntityId(mapData.MapId);
var mapGrid = Server.MapMan.CreateGridEntity(mapData.MapId);
mapData.MapGrid = mapGrid;
mapData.GridUid = mapGrid.Owner; // Fixing this requires an engine PR.
mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
var plating = tileDefinitionManager["Plating"];
mapData.MapUid = Server.System<SharedMapSystem>().CreateMap(out mapData.MapId, runMapInit: initialized);
mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
mapData.MapGrid.SetTile(mapData.GridCoords, platingTile);
mapData.Grid.Comp.SetTile(mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapData.MapGrid.GetAllTiles().First();
mapData.Tile = mapData.Grid.Comp.GetAllTiles().First();
});
TestMap = mapData;
if (!Settings.Connected)
return mapData;
await RunTicksSync(10);
mapData.CMapUid = ToClientUid(mapData.MapUid);
mapData.CGridUid = ToClientUid(mapData.GridUid);
mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
TestMap = mapData;
return mapData;
}

View File

@@ -131,7 +131,7 @@ public sealed partial class TestPair : IAsyncDisposable
// Move to pre-round lobby. Required to toggle dummy ticker on and off
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server.");
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
Assert.That(gameTicker.DummyTicker, Is.False);
Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
await Server.WaitPost(() => gameTicker.RestartRound());
@@ -146,6 +146,7 @@ public sealed partial class TestPair : IAsyncDisposable
// Restart server.
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server again");
await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);

View File

@@ -32,8 +32,8 @@ public sealed class AddTests
var guid = Guid.NewGuid();
var testMap = await pair.CreateTestMap();
var coordinates = testMap.GridCoords;
await pair.CreateTestMap();
var coordinates = pair.TestMap.GridCoords;
await server.WaitPost(() =>
{
var entity = sEntities.SpawnEntity(null, coordinates);

View File

@@ -212,7 +212,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork
DeviceNetworkComponent networkComponent1 = null;
DeviceNetworkComponent networkComponent2 = null;
WiredNetworkComponent wiredNetworkComponent = null;
var grid = testMap.MapGrid;
var grid = testMap.Grid.Comp;
var testValue = "test";
var payload = new NetworkPayload

View File

@@ -3,8 +3,6 @@ using Content.IntegrationTests.Tests.Construction.Interaction;
using Content.IntegrationTests.Tests.Interaction;
using Content.IntegrationTests.Tests.Weldable;
using Content.Shared.Tools.Components;
using Content.Server.Tools.Components;
using Content.Shared.DoAfter;
namespace Content.IntegrationTests.Tests.DoAfter;

View File

@@ -354,41 +354,18 @@ namespace Content.IntegrationTests.Tests
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var componentFactory = server.ResolveDependency<IComponentFactory>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
var mapSystem = entityManager.System<SharedMapSystem>();
var logmill = server.ResolveDependency<ILogManager>().GetSawmill("EntityTest");
Entity<MapGridComponent> grid = default!;
await server.WaitPost(() =>
{
// Create a one tile grid to stave off the grid 0 monsters
var mapId = mapManager.CreateMap();
mapManager.AddUninitializedMap(mapId);
grid = mapManager.CreateGridEntity(mapId);
var tileDefinition = tileDefinitionManager["Plating"];
var tile = new Tile(tileDefinition.TileId);
var coordinates = new EntityCoordinates(grid.Owner, Vector2.Zero);
mapSystem.SetTile(grid.Owner, grid.Comp!, coordinates, tile);
mapManager.DoMapInitialize(mapId);
});
await pair.CreateTestMap();
await server.WaitRunTicks(5);
var testLocation = pair.TestMap.GridCoords;
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
var testLocation = new EntityCoordinates(grid.Owner, Vector2.Zero);
foreach (var type in componentFactory.AllRegisteredTypes)
{

View File

@@ -46,17 +46,14 @@ namespace Content.IntegrationTests.Tests.Fluids
var server = pair.Server;
var testMap = await pair.CreateTestMap();
var grid = testMap.Grid.Comp;
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
MapGridComponent grid = null;
// Remove all tiles
await server.WaitPost(() =>
{
grid = testMap.MapGrid;
foreach (var tile in grid.GetAllTiles())
{
grid.SetTile(tile.GridIndices, Tile.Empty);

View File

@@ -989,7 +989,7 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task AddGravity(EntityUid? uid = null)
{
var target = uid ?? MapData.GridUid;
var target = uid ?? MapData.Grid;
await Server.WaitPost(() =>
{
var gravity = SEntMan.EnsureComponent<GravityComponent>(target);

View File

@@ -184,7 +184,7 @@ public abstract partial class InteractionTest
await Pair.CreateTestMap();
PlayerCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
TargetCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
await SetTile(Plating, grid: MapData.MapGrid);
await SetTile(Plating, grid: MapData.Grid.Comp);
// Get player data
var sPlayerMan = Server.ResolveDependency<Robust.Server.Player.IPlayerManager>();

View File

@@ -31,7 +31,7 @@ public abstract class MovementTest : InteractionTest
for (var i = -Tiles; i <= Tiles; i++)
{
await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.MapGrid);
await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.Grid.Comp);
}
AssertGridCount(1);

View File

@@ -183,7 +183,7 @@ public sealed class MaterialArbitrageTest
var spawnedPrice = await GetSpawnedPrice(spawnedEnts);
var price = await GetPrice(id);
if (spawnedPrice > 0 && price > 0)
Assert.That(spawnedPrice, Is.LessThanOrEqualTo(price), $"{id} increases in price after being destroyed");
Assert.That(spawnedPrice, Is.LessThanOrEqualTo(price), $"{id} increases in price after being destroyed\nEntities spawned on destruction: {string.Join(',', spawnedEnts)}");
// Check lathe production
if (latheRecipes.TryGetValue(id, out var recipe))
@@ -359,7 +359,7 @@ public sealed class MaterialArbitrageTest
{
var ent = entManager.SpawnEntity(id, testMap.GridCoords);
stackSys.SetCount(ent, 1);
priceCache[id] = price = pricing.GetPrice(ent);
priceCache[id] = price = pricing.GetPrice(ent, false);
entManager.DeleteEntity(ent);
});
}

View File

@@ -63,7 +63,8 @@ namespace Content.IntegrationTests.Tests
"MeteorArena",
//"Atlas",
//"Reach",
//"Train"
//"Train",
//"Oasis"
//CrystallPunk maps
"CaveArena",
@@ -158,7 +159,10 @@ namespace Content.IntegrationTests.Tests
[Test, TestCaseSource(nameof(GameMaps))]
public async Task GameMapsLoadableTest(string mapProto)
{
await using var pair = await PoolManager.GetServerClient();
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true // Stations spawn a bunch of nullspace entities and maps like centcomm.
});
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();

View File

@@ -38,31 +38,15 @@ public sealed class PrototypeSaveTest
var mapManager = server.ResolveDependency<IMapManager>();
var entityMan = server.ResolveDependency<IEntityManager>();
var prototypeMan = server.ResolveDependency<IPrototypeManager>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
var seriMan = server.ResolveDependency<ISerializationManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var prototypes = new List<EntityPrototype>();
MapGridComponent grid = default!;
EntityUid uid;
MapId mapId = default;
//Build up test environment
await server.WaitPost(() =>
{
// Create a one tile grid to stave off the grid 0 monsters
mapId = mapManager.CreateMap();
mapManager.AddUninitializedMap(mapId);
grid = mapManager.CreateGrid(mapId);
var tileDefinition = tileDefinitionManager["FloorSteel"]; // Wires n such disable ambiance while under the floor
var tile = new Tile(tileDefinition.TileId);
var coordinates = grid.Owner.ToCoordinates();
grid.SetTile(coordinates, tile);
});
await pair.CreateTestMap(false, "FloorSteel"); // Wires n such disable ambiance while under the floor
var mapId = pair.TestMap.MapId;
var grid = pair.TestMap.Grid;
await server.WaitRunTicks(5);

View File

@@ -39,7 +39,7 @@ public sealed class DockTest : ContentUnitTest
await server.WaitAssertion(() =>
{
entManager.DeleteEntity(map.GridUid);
entManager.DeleteEntity(map.Grid);
var grid1 = mapManager.CreateGridEntity(mapId);
var grid2 = mapManager.CreateGridEntity(mapId);
var grid1Ent = grid1.Owner;
@@ -104,7 +104,7 @@ public sealed class DockTest : ContentUnitTest
// Spawn shuttle and affirm no valid docks.
await server.WaitAssertion(() =>
{
entManager.DeleteEntity(map.GridUid);
entManager.DeleteEntity(map.Grid);
Assert.That(entManager.System<MapLoaderSystem>().TryLoad(otherMap.MapId, "/Maps/Shuttles/emergency.yml", out var rootUids));
shuttle = rootUids[0];

View File

@@ -0,0 +1,114 @@
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Shuttles.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
namespace Content.IntegrationTests.Tests.Station;
[TestFixture]
[TestOf(typeof(EmergencyShuttleSystem))]
public sealed class EvacShuttleTest
{
/// <summary>
/// Ensure that the emergency shuttle can be called, and that it will travel to centcomm
/// </summary>
[Test]
public async Task EmergencyEvacTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Dirty = true });
var server = pair.Server;
var entMan = server.EntMan;
var ticker = server.System<GameTicker>();
// Dummy ticker tests should not have centcomm
Assert.That(entMan.Count<StationCentcommComponent>(), Is.Zero);
var shuttleEnabled = pair.Server.CfgMan.GetCVar(CCVars.EmergencyShuttleEnabled);
pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern");
pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true);
await server.WaitPost(() => ticker.RestartRound());
await pair.RunTicksSync(25);
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
// Find the station, centcomm, and shuttle, and ftl map.
Assert.That(entMan.Count<StationCentcommComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<StationEmergencyShuttleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<StationDataComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EmergencyShuttleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<FTLMapComponent>(), Is.EqualTo(0));
var station = (Entity<StationCentcommComponent>) entMan.AllComponentsList<StationCentcommComponent>().Single();
var data = entMan.GetComponent<StationDataComponent>(station);
var shuttleData = entMan.GetComponent<StationEmergencyShuttleComponent>(station);
var saltern = data.Grids.Single();
Assert.That(entMan.HasComponent<MapGridComponent>(saltern));
var shuttle = shuttleData.EmergencyShuttle!.Value;
Assert.That(entMan.HasComponent<EmergencyShuttleComponent>(shuttle));
Assert.That(entMan.HasComponent<MapGridComponent>(shuttle));
var centcomm = station.Comp.Entity!.Value;
Assert.That(entMan.HasComponent<MapGridComponent>(centcomm));
var centcommMap = station.Comp.MapEntity!.Value;
Assert.That(entMan.HasComponent<MapComponent>(centcommMap));
Assert.That(server.Transform(centcomm).MapUid, Is.EqualTo(centcommMap));
var salternXform = server.Transform(saltern);
Assert.That(salternXform.MapUid, Is.Not.Null);
Assert.That(salternXform.MapUid, Is.Not.EqualTo(centcommMap));
var shuttleXform = server.Transform(shuttle);
Assert.That(shuttleXform.MapUid, Is.Not.Null);
Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap));
// Set up shuttle timing
var evacSys = server.System<EmergencyShuttleSystem>();
evacSys.TransitTime = ShuttleSystem.DefaultTravelTime; // Absolute minimum transit time, so the test has to run for at least this long
// TODO SHUTTLE fix spaghetti
var dockTime = server.CfgMan.GetCVar(CCVars.EmergencyShuttleDockTime);
server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, 2);
async Task RunSeconds(float seconds)
{
await pair.RunTicksSync((int) Math.Ceiling(seconds / server.Timing.TickPeriod.TotalSeconds));
}
// Call evac shuttle.
await pair.WaitCommand("callshuttle 0:02");
await RunSeconds(3);
// Shuttle should have arrived on the station
Assert.That(shuttleXform.MapUid, Is.EqualTo(salternXform.MapUid));
await RunSeconds(2);
// Shuttle should be FTLing back to centcomm
Assert.That(entMan.Count<FTLMapComponent>(), Is.EqualTo(1));
var ftl = (Entity<FTLMapComponent>) entMan.AllComponentsList<FTLMapComponent>().Single();
Assert.That(entMan.HasComponent<MapComponent>(ftl));
Assert.That(ftl.Owner, Is.Not.EqualTo(centcommMap));
Assert.That(ftl.Owner, Is.Not.EqualTo(salternXform.MapUid));
Assert.That(shuttleXform.MapUid, Is.EqualTo(ftl.Owner));
// Shuttle should have arrived at centcomm
await RunSeconds(ShuttleSystem.DefaultTravelTime);
Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap));
// Round should be ending now
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PostRound));
server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime);
pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, shuttleEnabled);
await pair.CleanReturnAsync();
}
}

View File

@@ -37,7 +37,7 @@ public sealed class TileConstructionTests : InteractionTest
// Remove grid
await SetTile(null);
await SetTile(null, PlayerCoords);
Assert.That(MapData.MapGrid.Deleted);
Assert.That(MapData.Grid.Comp.Deleted);
AssertGridCount(0);
// Place Lattice
@@ -70,7 +70,7 @@ public sealed class TileConstructionTests : InteractionTest
// Remove grid
await SetTile(null);
await SetTile(null, PlayerCoords);
Assert.That(MapData.MapGrid.Deleted);
Assert.That(MapData.Grid.Comp.Deleted);
AssertGridCount(0);
// Space -> Lattice

View File

@@ -111,7 +111,7 @@ public sealed partial class AdminVerbSystem
{
Text = Loc.GetString("admin-verb-text-make-thief"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/ihscombat.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
Act = () =>
{
_thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad

View File

@@ -12,6 +12,7 @@ namespace Content.Server.Atmos.EntitySystems
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public override void Initialize()
{
@@ -59,12 +60,14 @@ namespace Content.Server.Atmos.EntitySystems
var gridId = xform.GridUid;
var coords = xform.Coordinates;
var tilePos = grid.TileIndicesFor(coords);
var tilePos = _mapSystem.TileIndicesFor(gridId.Value, grid, coords);
// Update and invalidate new position.
airtight.LastPosition = (gridId.Value, tilePos);
InvalidatePosition(gridId.Value, tilePos);
var airtightEv = new AirtightChanged(uid, airtight, (gridId.Value, tilePos));
RaiseLocalEvent(uid, ref airtightEv, true);
}
private void OnAirtightReAnchor(EntityUid uid, AirtightComponent airtight, ref ReAnchorEvent args)
@@ -74,6 +77,9 @@ namespace Content.Server.Atmos.EntitySystems
// Update and invalidate new position.
airtight.LastPosition = (gridId, args.TilePos);
InvalidatePosition(gridId, args.TilePos);
var airtightEv = new AirtightChanged(uid, airtight, (gridId, args.TilePos));
RaiseLocalEvent(uid, ref airtightEv, true);
}
}
@@ -153,6 +159,5 @@ namespace Content.Server.Atmos.EntitySystems
}
[ByRefEvent]
public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight,
(EntityUid Grid, Vector2i Tile) Position);
public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight, (EntityUid Grid, Vector2i Tile) Position);
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Administration;
using Content.Server.Atmos.Components;
using Content.Shared.Administration;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -84,44 +85,72 @@ public sealed partial class AtmosphereSystem
continue;
}
var transform = Transform(euid.Value);
// Force Invalidate & update air on all tiles
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
new(euid.Value, gridAtmosphere, Comp<GasTileOverlayComponent>(euid.Value), gridComp, Transform(euid.Value));
foreach (var (indices, tileMain) in gridAtmosphere.Tiles)
RebuildGridTiles(grid);
var query = GetEntityQuery<AtmosFixMarkerComponent>();
foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
{
var tile = tileMain.Air;
if (tile == null)
if (tile.Air is not {Immutable: false} air)
continue;
if (!_mapSystem.TryGetTile(gridComp, indices, out var gTile) || gTile.IsEmpty)
{
gridAtmosphere.Tiles.Remove(indices);
continue;
}
if (tile.Immutable && !IsTileSpace(euid, transform.MapUid, indices))
{
tile = new GasMixture(tile.Volume) { Temperature = tile.Temperature };
tileMain.Air = tile;
}
tile.Clear();
air.Clear();
var mixtureId = 0;
foreach (var entUid in gridComp.GetAnchoredEntities(indices))
var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices);
while (enumerator.MoveNext(out var entUid))
{
if (!TryComp(entUid, out AtmosFixMarkerComponent? afm))
continue;
mixtureId = afm.Mode;
break;
if (query.TryComp(entUid, out var marker))
mixtureId = marker.Mode;
}
var mixture = mixtures[mixtureId];
Merge(tile, mixture);
tile.Temperature = mixture.Temperature;
gridAtmosphere.InvalidatedCoords.Add(indices);
var mixture = mixtures[mixtureId];
Merge(air, mixture);
air.Temperature = mixture.Temperature;
}
}
}
/// <summary>
/// Clears & re-creates all references to <see cref="TileAtmosphere"/>s stored on a grid.
/// </summary>
private void RebuildGridTiles(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent)
{
foreach (var indices in ent.Comp1.Tiles.Keys)
{
InvalidateVisuals((ent, ent), indices);
}
var atmos = ent.Comp1;
atmos.MapTiles.Clear();
atmos.ActiveTiles.Clear();
atmos.ExcitedGroups.Clear();
atmos.HotspotTiles.Clear();
atmos.SuperconductivityTiles.Clear();
atmos.HighPressureDelta.Clear();
atmos.CurrentRunTiles.Clear();
atmos.CurrentRunExcitedGroups.Clear();
atmos.InvalidatedCoords.Clear();
atmos.CurrentRunInvalidatedTiles.Clear();
atmos.PossiblyDisconnectedTiles.Clear();
atmos.Tiles.Clear();
var volume = GetVolumeForTiles(ent);
TryComp(ent.Comp4.MapUid, out MapAtmosphereComponent? mapAtmos);
var enumerator = _map.GetAllTilesEnumerator(ent, ent);
while (enumerator.MoveNext(out var tileRef))
{
var tile = GetOrNewTile(ent, ent, tileRef.Value.GridIndices);
UpdateTileData(ent, mapAtmos, tile);
UpdateAdjacentTiles(ent, tile, activate: true);
UpdateTileAir(ent, tile, volume);
}
}
private CompletionResult FixGridAtmosCommandCompletions(IConsoleShell shell, string[] args)
{
MapId? playerMap = null;

View File

@@ -30,13 +30,15 @@ namespace Content.Server.Atmos.EntitySystems
private int _currentRunAtmosphereIndex;
private bool _simulationPaused;
private TileAtmosphere GetOrNewTile(EntityUid owner, GridAtmosphereComponent atmosphere, Vector2i index)
private TileAtmosphere GetOrNewTile(EntityUid owner, GridAtmosphereComponent atmosphere, Vector2i index, bool invalidateNew = true)
{
var tile = atmosphere.Tiles.GetOrNew(index, out var existing);
if (existing)
return tile;
atmosphere.InvalidatedCoords.Add(index);
if (invalidateNew)
atmosphere.InvalidatedCoords.Add(index);
tile.GridIndex = owner;
tile.GridIndices = index;
return tile;
@@ -68,7 +70,7 @@ namespace Content.Server.Atmos.EntitySystems
atmosphere.CurrentRunInvalidatedTiles.EnsureCapacity(atmosphere.InvalidatedCoords.Count);
foreach (var indices in atmosphere.InvalidatedCoords)
{
var tile = GetOrNewTile(uid, atmosphere, indices);
var tile = GetOrNewTile(uid, atmosphere, indices, invalidateNew: false);
atmosphere.CurrentRunInvalidatedTiles.Enqueue(tile);
// Update tile.IsSpace and tile.MapAtmosphere, and tile.AirtightData.

View File

@@ -132,7 +132,7 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
{
if (message.Session.AttachedEntity is not {Valid: true})
if (message.Session.AttachedEntity is not { Valid: true })
return;
DisableAnalyzer(uid, component);
}
@@ -169,7 +169,7 @@ namespace Content.Server.Atmos.EntitySystems
// Check if position is out of range => don't update and disable
if (!component.LastPosition.Value.InRange(EntityManager, _transform, userPos, SharedInteractionSystem.InteractionRange))
{
if(component.User is { } userId && component.Enabled)
if (component.User is { } userId && component.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(uid, component, component.User);
return false;
@@ -182,13 +182,13 @@ namespace Content.Server.Atmos.EntitySystems
var tileMixture = _atmo.GetContainingMixture(uid, true);
if (tileMixture != null)
{
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Pressure, tileMixture.Temperature,
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
GenerateGasEntryArray(tileMixture)));
}
else
{
// No gases were found
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f));
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
}
var deviceFlipped = false;
@@ -209,8 +209,8 @@ namespace Content.Server.Atmos.EntitySystems
{
foreach (var mixes in ev.GasMixtures)
{
if(mixes.Value != null)
gasMixList.Add(new GasMixEntry(mixes.Key, mixes.Value.Pressure, mixes.Value.Temperature, GenerateGasEntryArray(mixes.Value)));
if (mixes.Item2 != null)
gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
}
deviceFlipped = ev.DeviceFlipped;
@@ -223,7 +223,16 @@ namespace Content.Server.Atmos.EntitySystems
foreach (var pair in node.Nodes)
{
if (pair.Value is PipeNode pipeNode)
gasMixList.Add(new GasMixEntry(pair.Key, pipeNode.Air.Pressure, pipeNode.Air.Temperature, GenerateGasEntryArray(pipeNode.Air)));
{
// check if the volume is zero for some reason so we don't divide by zero
if (pipeNode.Air.Volume == 0f)
continue;
// only display the gas in the analyzed pipe element, not the whole system
var pipeAir = pipeNode.Air.Clone();
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
pipeAir.Volume = pipeNode.Volume;
gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
}
}
}
}
@@ -286,9 +295,9 @@ namespace Content.Server.Atmos.EntitySystems
public sealed class GasAnalyzerScanEvent : EntityEventArgs
{
/// <summary>
/// Key is the mix name (ex "pipe", "inlet", "filter"), value is the pipe direction and GasMixture. Add all mixes that should be reported when scanned.
/// The string is for the name (ex "pipe", "inlet", "filter"), GasMixture for the corresponding gas mix. Add all mixes that should be reported when scanned.
/// </summary>
public Dictionary<string, GasMixture?>? GasMixtures;
public List<(string, GasMixture?)>? GasMixtures;
/// <summary>
/// If the device is flipped. Flipped is defined as when the inline input is 90 degrees CW to the side input

View File

@@ -359,7 +359,8 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args)
{
args.GasMixtures = new Dictionary<string, GasMixture?> { {Name(uid), component.Air} };
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), component.Air));
}
private void OnGasTankPrice(EntityUid uid, GasTankComponent component, ref PriceCalculationEvent args)

View File

@@ -73,7 +73,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
if (filter.FilteredGas.HasValue)
{
var filteredOut = new GasMixture() {Temperature = removed.Temperature};
var filteredOut = new GasMixture() { Temperature = removed.Temperature };
filteredOut.SetMoles(filter.FilteredGas.Value, removed.GetMoles(filter.FilteredGas.Value));
removed.SetMoles(filter.FilteredGas.Value, 0f);
@@ -180,17 +180,30 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
/// </summary>
private void OnFilterAnalyzed(EntityUid uid, GasFilterComponent component, GasAnalyzerScanEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
return;
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures ??= new Dictionary<string, GasMixture?>();
if(_nodeContainer.TryGetNode(nodeContainer, component.InletName, out PipeNode? inlet))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-inlet"), inlet.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.FilterName, out PipeNode? filterNode))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-filter"), filterNode.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.OutletName, out PipeNode? outlet))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, component.InletName, out PipeNode? inlet) && inlet.Air.Volume != 0f)
{
var inletAirLocal = inlet.Air.Clone();
inletAirLocal.Multiply(inlet.Volume / inlet.Air.Volume);
inletAirLocal.Volume = inlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-inlet"), inletAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.FilterName, out PipeNode? filterNode) && filterNode.Air.Volume != 0f)
{
var filterNodeAirLocal = filterNode.Air.Clone();
filterNodeAirLocal.Multiply(filterNode.Volume / filterNode.Air.Volume);
filterNodeAirLocal.Volume = filterNode.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-filter"), filterNodeAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f)
{
var outletAirLocal = outlet.Air.Clone();
outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume);
outletAirLocal.Volume = outlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal));
}
args.DeviceFlipped = inlet != null && filterNode != null && inlet.CurrentPipeDirection.ToDirection() == filterNode.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
}

View File

@@ -205,19 +205,31 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
/// </summary>
private void OnMixerAnalyzed(EntityUid uid, GasMixerComponent component, GasAnalyzerScanEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
return;
args.GasMixtures ??= new List<(string, GasMixture?)>();
var gasMixDict = new Dictionary<string, GasMixture?>();
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, component.InletOneName, out PipeNode? inletOne) && inletOne.Air.Volume != 0f)
{
var inletOneAirLocal = inletOne.Air.Clone();
inletOneAirLocal.Multiply(inletOne.Volume / inletOne.Air.Volume);
inletOneAirLocal.Volume = inletOne.Volume;
args.GasMixtures.Add(($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOneAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.InletTwoName, out PipeNode? inletTwo) && inletTwo.Air.Volume != 0f)
{
var inletTwoAirLocal = inletTwo.Air.Clone();
inletTwoAirLocal.Multiply(inletTwo.Volume / inletTwo.Air.Volume);
inletTwoAirLocal.Volume = inletTwo.Volume;
args.GasMixtures.Add(($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwoAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f)
{
var outletAirLocal = outlet.Air.Clone();
outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume);
outletAirLocal.Volume = outlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal));
}
if(_nodeContainer.TryGetNode(nodeContainer, component.InletOneName, out PipeNode? inletOne))
gasMixDict.Add($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOne.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.InletTwoName, out PipeNode? inletTwo))
gasMixDict.Add($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwo.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.OutletName, out PipeNode? outlet))
gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
args.GasMixtures = gasMixDict;
args.DeviceFlipped = inletOne != null && inletTwo != null && inletOne.CurrentPipeDirection.ToDirection() == inletTwo.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
}
}

View File

@@ -294,9 +294,17 @@ public sealed class GasCanisterSystem : EntitySystem
/// <summary>
/// Returns the gas mixture for the gas analyzer
/// </summary>
private void OnAnalyzed(EntityUid uid, GasCanisterComponent component, GasAnalyzerScanEvent args)
private void OnAnalyzed(EntityUid uid, GasCanisterComponent canisterComponent, GasAnalyzerScanEvent args)
{
args.GasMixtures = new Dictionary<string, GasMixture?> { {Name(uid), component.Air} };
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), canisterComponent.Air));
// if a tank is inserted show it on the analyzer as well
if (canisterComponent.GasTankSlot.Item != null)
{
var tank = canisterComponent.GasTankSlot.Item.Value;
var tankComponent = Comp<GasTankComponent>(tank);
args.GasMixtures.Add((Name(tank), tankComponent.Air));
}
}
/// <summary>

View File

@@ -80,7 +80,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
}
var timeDelta = args.dt;
var timeDelta = args.dt;
var pressureDelta = timeDelta * vent.TargetPressureChange;
if (vent.PumpDirection == VentPumpDirection.Releasing && pipe.Air.Pressure > 0)
@@ -292,7 +292,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
/// </summary>
private void OnAnalyzed(EntityUid uid, GasVentPumpComponent component, GasAnalyzerScanEvent args)
{
var gasMixDict = new Dictionary<string, GasMixture?>();
args.GasMixtures ??= new List<(string, GasMixture?)>();
// these are both called pipe, above it switches using this so I duplicated that...?
var nodeName = component.PumpDirection switch
@@ -301,10 +301,14 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
VentPumpDirection.Siphoning => component.Outlet,
_ => throw new ArgumentOutOfRangeException()
};
if (_nodeContainer.TryGetNode(uid, nodeName, out PipeNode? pipe))
gasMixDict.Add(nodeName, pipe.Air);
args.GasMixtures = gasMixDict;
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, nodeName, out PipeNode? pipe) && pipe.Air.Volume != 0f)
{
var pipeAirLocal = pipe.Air.Clone();
pipeAirLocal.Multiply(pipe.Volume / pipe.Air.Volume);
pipeAirLocal.Volume = pipe.Volume;
args.GasMixtures.Add((nodeName, pipeAirLocal));
}
}
private void OnWeldChanged(EntityUid uid, GasVentPumpComponent component, ref WeldableChangedEvent args)

View File

@@ -151,10 +151,8 @@ namespace Content.Server.Atmos.Portable
/// </summary>
private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent component, GasAnalyzerScanEvent args)
{
args.GasMixtures ??= new Dictionary<string, GasMixture?> { { Name(uid), component.Air } };
// If it's connected to a port, include the port side
if (_nodeContainer.TryGetNode(uid, component.PortName, out PipeNode? port))
args.GasMixtures.Add(component.PortName, port.Air);
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), component.Air));
}
}
}

View File

@@ -0,0 +1,152 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Audio.Jukebox;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using JukeboxComponent = Content.Shared.Audio.Jukebox.JukeboxComponent;
namespace Content.Server.Audio.Jukebox;
public sealed class JukeboxSystem : SharedJukeboxSystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JukeboxComponent, JukeboxSelectedMessage>(OnJukeboxSelected);
SubscribeLocalEvent<JukeboxComponent, JukeboxPlayingMessage>(OnJukeboxPlay);
SubscribeLocalEvent<JukeboxComponent, JukeboxPauseMessage>(OnJukeboxPause);
SubscribeLocalEvent<JukeboxComponent, JukeboxStopMessage>(OnJukeboxStop);
SubscribeLocalEvent<JukeboxComponent, JukeboxSetTimeMessage>(OnJukeboxSetTime);
SubscribeLocalEvent<JukeboxComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<JukeboxComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<JukeboxComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnComponentInit(EntityUid uid, JukeboxComponent component, ComponentInit args)
{
if (HasComp<ApcPowerReceiverComponent>(uid))
{
TryUpdateVisualState(uid, component);
}
}
private void OnJukeboxPlay(EntityUid uid, JukeboxComponent component, ref JukeboxPlayingMessage args)
{
if (Exists(component.AudioStream))
{
Audio.SetState(component.AudioStream, AudioState.Playing);
}
else
{
component.AudioStream = Audio.Stop(component.AudioStream);
if (string.IsNullOrEmpty(component.SelectedSongId) ||
!_protoManager.TryIndex(component.SelectedSongId, out var jukeboxProto))
{
return;
}
component.AudioStream = Audio.PlayPvs(jukeboxProto.Path, uid, AudioParams.Default.WithMaxDistance(10f))?.Entity;
Dirty(uid, component);
}
}
private void OnJukeboxPause(Entity<JukeboxComponent> ent, ref JukeboxPauseMessage args)
{
Audio.SetState(ent.Comp.AudioStream, AudioState.Paused);
}
private void OnJukeboxSetTime(EntityUid uid, JukeboxComponent component, JukeboxSetTimeMessage args)
{
var offset = (args.Session.Channel.Ping * 1.5f) / 1000f;
Audio.SetPlaybackPosition(component.AudioStream, args.SongTime + offset);
}
private void OnPowerChanged(Entity<JukeboxComponent> entity, ref PowerChangedEvent args)
{
TryUpdateVisualState(entity);
if (!this.IsPowered(entity.Owner, EntityManager))
{
Stop(entity);
}
}
private void OnJukeboxStop(Entity<JukeboxComponent> entity, ref JukeboxStopMessage args)
{
Stop(entity);
}
private void Stop(Entity<JukeboxComponent> entity)
{
Audio.SetState(entity.Comp.AudioStream, AudioState.Stopped);
Dirty(entity);
}
private void OnJukeboxSelected(EntityUid uid, JukeboxComponent component, JukeboxSelectedMessage args)
{
if (!Audio.IsPlaying(component.AudioStream))
{
component.SelectedSongId = args.SongId;
DirectSetVisualState(uid, JukeboxVisualState.Select);
component.Selecting = true;
component.AudioStream = Audio.Stop(component.AudioStream);
}
Dirty(uid, component);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<JukeboxComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Selecting)
{
comp.SelectAccumulator += frameTime;
if (comp.SelectAccumulator >= 0.5f)
{
comp.SelectAccumulator = 0f;
comp.Selecting = false;
TryUpdateVisualState(uid, comp);
}
}
}
}
private void OnComponentShutdown(EntityUid uid, JukeboxComponent component, ComponentShutdown args)
{
component.AudioStream = Audio.Stop(component.AudioStream);
}
private void DirectSetVisualState(EntityUid uid, JukeboxVisualState state)
{
_appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, state);
}
private void TryUpdateVisualState(EntityUid uid, JukeboxComponent? jukeboxComponent = null)
{
if (!Resolve(uid, ref jukeboxComponent))
return;
var finalState = JukeboxVisualState.On;
if (!this.IsPowered(uid, EntityManager))
{
finalState = JukeboxVisualState.Off;
}
_appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, finalState);
}
}

View File

@@ -2,16 +2,16 @@ using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
using Robust.Shared.Audio;
namespace Content.Server.Botany;
[Prototype("seed")]
public sealed class SeedPrototype : SeedData, IPrototype
public sealed partial class SeedPrototype : SeedData, IPrototype
{
[IdDataField] public string ID { get; private init; } = default!;
}

View File

@@ -184,6 +184,15 @@ namespace Content.Server.Cargo.Systems
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
_audio.PlayPvs(component.ConfirmSound, uid);
var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id");
var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id");
var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast",
("productName", Loc.GetString(order.ProductName)),
("orderAmount", order.OrderQuantity),
("approverName", approverName),
("approverJob", approverJob),
("cost", cost));
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName)));
// Log order approval
@@ -327,7 +336,7 @@ namespace Content.Server.Cargo.Systems
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)
{
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
}
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component)
@@ -376,6 +385,7 @@ namespace Content.Server.Cargo.Systems
public bool AddAndApproveOrder(
EntityUid dbUid,
string spawnId,
string name,
int cost,
int qty,
string sender,
@@ -388,7 +398,7 @@ namespace Content.Server.Cargo.Systems
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
// Make an order
var id = GenerateOrderId(component);
var order = new CargoOrderData(id, spawnId, cost, qty, sender, description);
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description);
// Approve it now
order.SetApproverData(dest, sender);

View File

@@ -154,7 +154,7 @@ public sealed partial class CargoSystem
// We won't be able to fit the whole order on, so make one
// which represents the space we do have left:
var reducedOrder = new CargoOrderData(order.OrderId,
order.ProductId, order.Price, spaceRemaining, order.Requester, order.Reason);
order.ProductId, order.ProductName, order.Price, spaceRemaining, order.Requester, order.Reason);
orders.Add(reducedOrder);
}
else

View File

@@ -8,6 +8,7 @@ using Content.Server.Stack;
using Content.Server.Station.Systems;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
@@ -42,6 +43,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly RadioSystem _radio = default!;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<CargoSellBlacklistComponent> _blacklistQuery;

View File

@@ -199,7 +199,7 @@ public sealed class PricingSystem : EntitySystem
/// This fires off an event to calculate the price.
/// Calculating the price of an entity that somehow contains itself will likely hang.
/// </remarks>
public double GetPrice(EntityUid uid)
public double GetPrice(EntityUid uid, bool includeContents = true)
{
var ev = new PriceCalculationEvent();
RaiseLocalEvent(uid, ref ev);
@@ -222,7 +222,7 @@ public sealed class PricingSystem : EntitySystem
price += GetStaticPrice(uid);
}
if (TryComp<ContainerManagerComponent>(uid, out var containers))
if (includeContents && TryComp<ContainerManagerComponent>(uid, out var containers))
{
foreach (var container in containers.Containers.Values)
{

View File

@@ -0,0 +1,36 @@
using System.Diagnostics;
using Content.Server.Administration;
using Content.Server.Chat.V2.Repository;
using Content.Shared.Administration;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Content.Server.Chat.V2.Commands;
[ToolshedCommand, AdminCommand(AdminFlags.Admin)]
public sealed class DeleteChatMessageCommand : ToolshedCommand
{
[Dependency] private readonly IEntitySystemManager _manager = default!;
[CommandImplementation("id")]
public void DeleteChatMessage([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] uint messageId)
{
if (!_manager.GetEntitySystem<ChatRepositorySystem>().Delete(messageId))
{
ctx.ReportError(new MessageIdDoesNotExist());
}
}
}
public record struct MessageIdDoesNotExist() : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromUnformatted(Loc.GetString("command-error-deletechatmessage-id-notexist"));
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,41 @@
using System.Diagnostics;
using Content.Server.Administration;
using Content.Server.Chat.V2.Repository;
using Content.Shared.Administration;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Content.Server.Chat.V2.Commands;
[ToolshedCommand, AdminCommand(AdminFlags.Admin)]
public sealed class NukeChatMessagesCommand : ToolshedCommand
{
[Dependency] private readonly IEntitySystemManager _manager = default!;
[CommandImplementation("usernames")]
public void Command([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string usernamesCsv)
{
var usernames = usernamesCsv.Split(',');
foreach (var username in usernames)
{
if (!_manager.GetEntitySystem<ChatRepositorySystem>().NukeForUsername(username, out var reason))
{
ctx.ReportError(new NukeMessagesForUsernameError(reason));
}
}
}
}
public record struct NukeMessagesForUsernameError(string Reason) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromUnformatted(Reason);
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,94 @@
using Content.Shared.Chat.Prototypes;
using Content.Shared.Chat.V2;
using Content.Shared.Radio;
namespace Content.Server.Chat.V2;
/// <summary>
/// Raised locally when a comms announcement is made.
/// </summary>
public sealed class CommsAnnouncementCreatedEvent(EntityUid sender, EntityUid console, string message) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = sender;
public string Message { get; set; } = message;
public MessageType Type => MessageType.Announcement;
public EntityUid Console = console;
}
/// <summary>
/// Raised locally when a character speaks in Dead Chat.
/// </summary>
public sealed class DeadChatCreatedEvent(EntityUid speaker, string message, bool isAdmin) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = speaker;
public string Message { get; set; } = message;
public MessageType Type => MessageType.DeadChat;
public bool IsAdmin = isAdmin;
}
/// <summary>
/// Raised locally when a character emotes.
/// </summary>
public sealed class EmoteCreatedEvent(EntityUid sender, string message, float range) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = sender;
public string Message { get; set; } = message;
public MessageType Type => MessageType.Emote;
public float Range = range;
}
/// <summary>
/// Raised locally when a character talks in local.
/// </summary>
public sealed class LocalChatCreatedEvent(EntityUid speaker, string message, float range) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = speaker;
public string Message { get; set; } = message;
public MessageType Type => MessageType.Local;
public float Range = range;
}
/// <summary>
/// Raised locally when a character speaks in LOOC.
/// </summary>
public sealed class LoocCreatedEvent(EntityUid speaker, string message) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = speaker;
public string Message { get; set; } = message;
public MessageType Type => MessageType.Looc;
}
/// <summary>
/// Raised locally when a character speaks on the radio.
/// </summary>
public sealed class RadioCreatedEvent(
EntityUid speaker,
string message,
RadioChannelPrototype channel)
: IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = speaker;
public string Message { get; set; } = message;
public RadioChannelPrototype Channel = channel;
public MessageType Type => MessageType.Radio;
}
/// <summary>
/// Raised locally when a character whispers.
/// </summary>
public sealed class WhisperCreatedEvent(EntityUid speaker, string message, float minRange, float maxRange) : IChatEvent
{
public uint Id { get; set; }
public EntityUid Sender { get; set; } = speaker;
public string Message { get; set; } = message;
public MessageType Type => MessageType.Whisper;
public float MinRange = minRange;
public float MaxRange = maxRange;
}

View File

@@ -0,0 +1,196 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using Content.Shared.Chat.V2;
using Content.Shared.Chat.V2.Repository;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Replays;
namespace Content.Server.Chat.V2.Repository;
/// <summary>
/// Stores <see cref="IChatEvent"/>, gives them UIDs, and issues <see cref="MessageCreatedEvent"/>.
/// Allows for deletion of messages.
/// </summary>
public sealed class ChatRepositorySystem : EntitySystem
{
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IPlayerManager _player = default!;
// Clocks should start at 1, as 0 indicates "clock not set" or "clock forgotten to be set by bad programmer".
private uint _nextMessageId = 1;
private Dictionary<uint, ChatRecord> _messages = new();
private Dictionary<NetUserId, List<uint>> _playerMessages = new();
public override void Initialize()
{
Refresh();
_replay.RecordingFinished += _ =>
{
// TODO: resolve https://github.com/space-wizards/space-station-14/issues/25485 so we can dump the chat to disc.
Refresh();
};
}
/// <summary>
/// Adds an <see cref="IChatEvent"/> to the repo and raises it with a UID for consumption elsewhere.
/// </summary>
/// <param name="ev">The event to store and raise</param>
/// <returns>If storing and raising succeeded.</returns>
public bool Add(IChatEvent ev)
{
if (!_player.TryGetSessionByEntity(ev.Sender, out var session))
{
return false;
}
var messageId = _nextMessageId;
_nextMessageId++;
ev.Id = messageId;
var storedEv = new ChatRecord
{
UserName = session.Name,
UserId = session.UserId,
EntityName = Name(ev.Sender),
StoredEvent = ev
};
_messages[messageId] = storedEv;
CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, storedEv.UserId, out _)?.Add(messageId);
RaiseLocalEvent(ev.Sender, new MessageCreatedEvent(ev), true);
return true;
}
/// <summary>
/// Returns the event associated with a UID, if it exists.
/// </summary>
/// <param name="id">The UID of a event.</param>
/// <returns>The event, if it exists.</returns>
public IChatEvent? GetEventFor(uint id)
{
return _messages.TryGetValue(id, out var record) ? record.StoredEvent : null;
}
/// <summary>
/// Edits a specific message and issues a <see cref="MessagePatchedEvent"/> that says this happened both locally and
/// on the network. Note that this doesn't replay the message (yet), so translators and mutators won't act on it.
/// </summary>
/// <param name="id">The ID to edit</param>
/// <param name="message">The new message to send</param>
/// <returns>If patching did anything did anything</returns>
/// <remarks>Should be used for admining and admemeing only.</remarks>
public bool Patch(uint id, string message)
{
if (!_messages.TryGetValue(id, out var ev))
{
return false;
}
ev.StoredEvent.Message = message;
RaiseLocalEvent(new MessagePatchedEvent(id, message));
return true;
}
/// <summary>
/// Deletes a message from the repository and issues a <see cref="MessageDeletedEvent"/> that says this has happened
/// both locally and on the network.
/// </summary>
/// <param name="id">The ID to delete</param>
/// <returns>If deletion did anything</returns>
/// <remarks>Should only be used for adminning</remarks>
public bool Delete(uint id)
{
if (!_messages.TryGetValue(id, out var ev))
{
return false;
}
_messages.Remove(id);
if (_playerMessages.TryGetValue(ev.UserId, out var set))
{
set.Remove(id);
}
RaiseLocalEvent(new MessageDeletedEvent(id));
return true;
}
/// <summary>
/// Nukes a user's entire chat history from the repo and issues a <see cref="MessageDeletedEvent"/> saying this has
/// happened.
/// </summary>
/// <param name="userName">The user ID to nuke.</param>
/// <param name="reason">Why nuking failed, if it did.</param>
/// <returns>If nuking did anything.</returns>
/// <remarks>Note that this could be a <b>very large</b> event, as we send every single event ID over the wire.
/// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of
/// client modders who could use that information to cheat/metagrudge/etc >:(</remarks>
public bool NukeForUsername(string userName, [NotNullWhen(false)] out string? reason)
{
if (!_player.TryGetUserId(userName, out var userId))
{
reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenotexist", ("username", userName));
return false;
}
return NukeForUserId(userId, out reason);
}
/// <summary>
/// Nukes a user's entire chat history from the repo and issues a <see cref="MessageDeletedEvent"/> saying this has
/// happened.
/// </summary>
/// <param name="userId">The user ID to nuke.</param>
/// <param name="reason">Why nuking failed, if it did.</param>
/// <returns>If nuking did anything.</returns>
/// <remarks>Note that this could be a <b>very large</b> event, as we send every single event ID over the wire.
/// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of
/// client modders who could use that information to cheat/metagrudge/etc >:(</remarks>
public bool NukeForUserId(NetUserId userId, [NotNullWhen(false)] out string? reason)
{
if (!_playerMessages.TryGetValue(userId, out var dict))
{
reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenomessages", ("userId", userId.UserId.ToString()));
return false;
}
foreach (var id in dict)
{
_messages.Remove(id);
}
var ev = new MessagesNukedEvent(dict);
CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, userId, out _)?.Clear();
RaiseLocalEvent(ev);
reason = null;
return true;
}
/// <summary>
/// Dumps held chat storage data and refreshes the repo.
/// </summary>
public void Refresh()
{
_nextMessageId = 1;
_messages.Clear();
_playerMessages.Clear();
}
}

View File

@@ -1,23 +0,0 @@
using Content.Shared.FixedPoint;
namespace Content.Server.Chemistry.Components
{
[RegisterComponent]
public sealed partial class ReagentTankComponent : Component
{
[DataField("transferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 TransferAmount { get; set; } = FixedPoint2.New(10);
[DataField("tankType")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentTankType TankType { get; set; } = ReagentTankType.Unspecified;
}
public enum ReagentTankType : byte
{
Unspecified,
Fuel
}
}

View File

@@ -30,7 +30,7 @@ public sealed partial class ReactionMixerSystem : EntitySystem
return;
}
if (!_solutionContainers.TryGetMixableSolution(args.Target.Value, out var solution))
if (!_solutionContainers.TryGetMixableSolution(args.Target.Value, out var solution, out _))
return;
_popup.PopupEntity(Loc.GetString(entity.Comp.MixMessage, ("mixed", Identity.Entity(args.Target.Value, EntityManager)), ("mixer", Identity.Entity(entity.Owner, EntityManager))), args.User, args.User);

View File

@@ -1,7 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Shared.Alert;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
namespace Content.Server.Clothing;
@@ -13,8 +12,8 @@ public sealed class MagbootsSystem : SharedMagbootsSystem
{
base.Initialize();
SubscribeLocalEvent<MagbootsComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<MagbootsComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<MagbootsComponent, ClothingGotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<MagbootsComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
}
protected override void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bool state, MagbootsComponent? component)
@@ -38,19 +37,13 @@ public sealed class MagbootsSystem : SharedMagbootsSystem
}
}
private void OnGotUnequipped(EntityUid uid, MagbootsComponent component, GotUnequippedEvent args)
private void OnGotUnequipped(EntityUid uid, MagbootsComponent component, ref ClothingGotUnequippedEvent args)
{
if (args.Slot == "shoes")
{
UpdateMagbootEffects(args.Equipee, uid, false, component);
}
UpdateMagbootEffects(args.Wearer, uid, false, component);
}
private void OnGotEquipped(EntityUid uid, MagbootsComponent component, GotEquippedEvent args)
private void OnGotEquipped(EntityUid uid, MagbootsComponent component, ref ClothingGotEquippedEvent args)
{
if (args.Slot == "shoes")
{
UpdateMagbootEffects(args.Equipee, uid, true, component);
}
UpdateMagbootEffects(args.Wearer, uid, true, component);
}
}

View File

@@ -1,22 +1,24 @@
using Content.Shared.Tools;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Prototypes;
namespace Content.Server.Construction.Components
namespace Content.Server.Construction.Components;
/// <summary>
/// Used for something that can be refined by welder.
/// For example, glass shard can be refined to glass sheet.
/// </summary>
[RegisterComponent]
public sealed partial class WelderRefinableComponent : Component
{
/// <summary>
/// Used for something that can be refined by welder.
/// For example, glass shard can be refined to glass sheet.
/// </summary>
[RegisterComponent]
public sealed partial class WelderRefinableComponent : Component
{
[DataField("refineResult")]
public HashSet<string>? RefineResult = new();
[DataField]
public HashSet<EntProtoId>? RefineResult = new();
[DataField("refineTime")]
public float RefineTime = 2f;
[DataField]
public float RefineTime = 2f;
[DataField("qualityNeeded", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string QualityNeeded = "Welding";
}
[DataField]
public float RefineFuel;
[DataField]
public ProtoId<ToolQualityPrototype> QualityNeeded = "Welding";
}

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