diff --git a/.envrc b/.envrc
index 3550a30f2d..5def8fd66a 100644
--- a/.envrc
+++ b/.envrc
@@ -1 +1,4 @@
+if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then
+ source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8="
+fi
use flake
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 627126c918..be6c490f74 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -7,7 +7,7 @@
/Content.*/Station/ @moonheart08
/Content.*/Maps/ @moonheart08
/Content.*/GameTicking/ @moonheart08
-/Resources/Server\ Info/ @moonheart08
+/Resources/ServerInfo/ @moonheart08
/Resources/engineCommandPerms.yml @moonheart08
/Resources/clientCommandPerms.yml @moonheart08
/Resources/Prototypes/species.yml @moonheart08
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index 08cdb8c832..3eac41d835 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -173,7 +173,7 @@ public void UnlinkAllActions()
public void LinkAllActions(ActionsComponent? actions = null)
{
var player = _playerManager.LocalPlayer?.ControlledEntity;
- if (player == null || !Resolve(player.Value, ref actions))
+ if (player == null || !Resolve(player.Value, ref actions, false))
{
return;
}
diff --git a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
new file mode 100644
index 0000000000..736e22f834
--- /dev/null
+++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Chemistry;
+
+namespace Content.Client.Chemistry.EntitySystems;
+
+///
+public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
+{
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnReceiveRegistryUpdate);
+ }
+
+ private void OnReceiveRegistryUpdate(ReagentGuideRegistryChangedEvent message)
+ {
+ var data = message.Changeset;
+ foreach (var remove in data.Removed)
+ {
+ Registry.Remove(remove);
+ }
+
+ foreach (var (key, val) in data.GuideEntries)
+ {
+ Registry[key] = val;
+ }
+ }
+}
diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index 22d639fc2f..81220e650d 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -60,26 +60,15 @@ public override void Initialize()
private void OnAppearanceUpdate(EntityUid uid, InventoryComponent component, ref AppearanceChangeEvent args)
{
// May need to update jumpsuit stencils if the sex changed. Also required to properly set the stencil on init
- // when sex is first loaded from the profile.
- if (!TryComp(uid, out SpriteComponent? sprite) || !sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var layer))
- return;
+ // This is when sex is first loaded from the profile.
- if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid)
- || humanoid.Sex != Sex.Female
- || !_inventorySystem.TryGetSlotEntity(uid, "jumpsuit", out var suit, component)
- || !TryComp(suit, out ClothingComponent? clothing))
- {
- sprite.LayerSetVisible(layer, false);
+ if (!TryComp(uid, out SpriteComponent? sprite) ||
+ !TryComp(uid, out HumanoidAppearanceComponent? humanoid) ||
+ !_inventorySystem.TryGetSlotEntity(uid, "jumpsuit", out var suit, component) ||
+ !TryComp(suit, out ClothingComponent? clothing))
return;
- }
- sprite.LayerSetState(layer, clothing.FemaleMask switch
- {
- FemaleClothingMask.NoMask => "female_none",
- FemaleClothingMask.UniformTop => "female_top",
- _ => "female_full",
- });
- sprite.LayerSetVisible(layer, true);
+ SetGenderedMask(sprite, humanoid, clothing);
}
private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args)
@@ -225,20 +214,12 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot
return;
}
- if (slot == "jumpsuit" && sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var suitLayer))
+ if (slot == "jumpsuit")
{
- if (TryComp(equipee, out HumanoidAppearanceComponent? humanoid) && humanoid.Sex == Sex.Female)
- {
- sprite.LayerSetState(suitLayer, clothingComponent.FemaleMask switch
- {
- FemaleClothingMask.NoMask => "female_none",
- FemaleClothingMask.UniformTop => "female_top",
- _ => "female_full",
- });
- sprite.LayerSetVisible(suitLayer, true);
- }
- else
- sprite.LayerSetVisible(suitLayer, false);
+ if (!TryComp(equipee, out HumanoidAppearanceComponent? humanoid))
+ return;
+
+ SetGenderedMask(sprite, humanoid, clothingComponent);
}
if (!_inventorySystem.TryGetSlot(equipee, slot, out var slotDef, inventory))
@@ -315,4 +296,49 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot
RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true);
}
+
+
+ ///
+ /// Sets a sprite's gendered mask based on gender (obviously).
+ ///
+ /// Sprite to modify
+ /// Humanoid, to get gender from
+ /// Clothing component, to get mask sprite type
+ private static void SetGenderedMask(SpriteComponent sprite, HumanoidAppearanceComponent humanoid, ClothingComponent clothing)
+ {
+ if (!sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var layer))
+ return;
+
+ ClothingMask? mask = null;
+ var prefix = "";
+
+ switch (humanoid.Sex)
+ {
+ case Sex.Male:
+ mask = clothing.MaleMask;
+ prefix = "male_";
+ break;
+ case Sex.Female:
+ mask = clothing.FemaleMask;
+ prefix = "female_";
+ break;
+ case Sex.Unsexed:
+ mask = clothing.UnisexMask;
+ prefix = "unisex_";
+ break;
+ }
+
+ if (mask != null)
+ {
+ sprite.LayerSetState(layer, mask switch
+ {
+ ClothingMask.NoMask => $"{prefix}none",
+ ClothingMask.UniformTop => $"{prefix}top",
+ _ => $"{prefix}full",
+ });
+ sprite.LayerSetVisible(layer, true);
+ }
+ else
+ sprite.LayerSetVisible(layer, false);
+ }
}
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index e17b3a6a2f..49383d9779 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -14,6 +14,7 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences;
using Content.Client.Radiation.Overlays;
+using Content.Client.Replay;
using Content.Client.Screenshot;
using Content.Client.Singularity;
using Content.Client.Stylesheets;
@@ -62,6 +63,7 @@ public sealed class EntryPoint : GameClient
[Dependency] private readonly ExtendedDisconnectInformationManager _extendedDisconnectInformation = default!;
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
[Dependency] private readonly ContentLocalizationManager _contentLoc = default!;
+ [Dependency] private readonly ContentReplayPlaybackManager _playbackMan = default!;
public override void Init()
{
@@ -131,6 +133,7 @@ public override void Init()
_ghostKick.Initialize();
_extendedDisconnectInformation.Initialize();
_jobRequirements.Initialize();
+ _playbackMan.Initialize();
//AUTOSCALING default Setup!
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);
@@ -154,7 +157,6 @@ public override void PostInit()
_overlayManager.AddOverlay(new SingularityOverlay());
_overlayManager.AddOverlay(new FlashOverlay());
_overlayManager.AddOverlay(new RadiationPulseOverlay());
-
_chatManager.Initialize();
_clientPreferencesManager.Initialize();
_euiManager.Initialize();
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index 0109f90cc5..cd09ff5fd1 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -16,6 +16,7 @@
using System.Linq;
using System.Threading;
using Content.Shared.Eye.Blinding.Components;
+using Robust.Client;
using static Content.Shared.Interaction.SharedInteractionSystem;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -28,6 +29,7 @@ public sealed class ExamineSystem : ExamineSystemShared
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly VerbSystem _verbSystem = default!;
+ [Dependency] private readonly IBaseClient _client = default!;
public const string StyleClassEntityTooltip = "entity-tooltip";
@@ -327,9 +329,9 @@ public void VerbButtonPressed(BaseButton.ButtonEventArgs obj)
}
}
- public void DoExamine(EntityUid entity, bool centeredOnCursor=true)
+ public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid? userOverride = null)
{
- var playerEnt = _playerManager.LocalPlayer?.ControlledEntity;
+ var playerEnt = userOverride ?? _playerManager.LocalPlayer?.ControlledEntity;
if (playerEnt == null)
return;
@@ -341,10 +343,10 @@ public void DoExamine(EntityUid entity, bool centeredOnCursor=true)
canSeeClearly = false;
OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget: canSeeClearly);
- if (entity.IsClientSide())
+ if (entity.IsClientSide()
+ || _client.RunLevel == ClientRunLevel.SinglePlayerGame) // i.e. a replay
{
message = GetExamineText(entity, playerEnt);
-
UpdateTooltipInfo(playerEnt.Value, entity, message);
}
else
diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs
index f1e29a2ea1..33a20c945a 100644
--- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs
+++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs
@@ -21,11 +21,18 @@ public sealed class ClientGameTicker : SharedGameTicker
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly BackgroundAudioSystem _backgroundAudio = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
[ViewVariables] private bool _initialized;
private Dictionary> _jobsAvailable = new();
private Dictionary _stationNames = new();
+ ///
+ /// The current round-end window. Could be used to support re-opening the window after closing it.
+ ///
+ private RoundEndSummaryWindow? _window;
+
[ViewVariables] public bool AreWeReady { get; private set; }
[ViewVariables] public bool IsGameStarted { get; private set; }
[ViewVariables] public string? LobbySong { get; private set; }
@@ -127,13 +134,17 @@ private void RoundEnd(RoundEndMessageEvent message)
if (message.LobbySong != null)
{
LobbySong = message.LobbySong;
- Get().StartLobbyMusic();
+ _backgroundAudio.StartLobbyMusic();
}
RestartSound = message.RestartSound;
+ // Don't open duplicate windows (mainly for replays).
+ if (_window?.RoundId == message.RoundId)
+ return;
+
//This is not ideal at all, but I don't see an immediately better fit anywhere else.
- var roundEnd = 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);
}
private void RoundRestartCleanup(RoundRestartCleanupEvent ev)
@@ -147,7 +158,7 @@ private void RoundRestartCleanup(RoundRestartCleanupEvent ev)
return;
}
- SoundSystem.Play(RestartSound, Filter.Empty());
+ _audio.PlayGlobal(RestartSound, Filter.Local(), false);
// Cleanup the sound, we only want it to play when the round restarts after it ends normally.
RestartSound = null;
diff --git a/Content.Client/Gameplay/GameplayState.cs b/Content.Client/Gameplay/GameplayState.cs
index 3765753c69..1efee978f3 100644
--- a/Content.Client/Gameplay/GameplayState.cs
+++ b/Content.Client/Gameplay/GameplayState.cs
@@ -13,7 +13,8 @@
namespace Content.Client.Gameplay
{
- public sealed class GameplayState : GameplayStateBase, IMainViewportState
+ [Virtual]
+ public class GameplayState : GameplayStateBase, IMainViewportState
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
diff --git a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
index 34b4f3bdaa..fc885b3655 100644
--- a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
@@ -79,7 +79,8 @@ protected override void KeyBindDown(GUIBoundKeyEventArgs args)
// do examination?
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
- _examineSystem.DoExamine(entity.Value);
+ _examineSystem.DoExamine(entity.Value,
+ userOverride: _guidebookSystem.GetGuidebookUser());
args.Handle();
return;
}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
new file mode 100644
index 0000000000..e8a36d57f4
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
new file mode 100644
index 0000000000..4b832be02b
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
@@ -0,0 +1,176 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Client.Chemistry.EntitySystems;
+using Content.Client.Guidebook.Richtext;
+using Content.Client.Message;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Chemistry.Reagent;
+using JetBrains.Annotations;
+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.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Guidebook.Controls;
+
+///
+/// Control for embedding a reagent into a guidebook.
+///
+[UsedImplicitly, GenerateTypedNameReferences]
+public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag
+{
+ [Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ private readonly ChemistryGuideDataSystem _chemistryGuideData;
+
+ public GuideReagentEmbed()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _chemistryGuideData = _systemManager.GetEntitySystem();
+ MouseFilter = MouseFilterMode.Stop;
+ }
+
+ public GuideReagentEmbed(string reagent) : this()
+ {
+ GenerateControl(_prototype.Index(reagent));
+ }
+
+ public GuideReagentEmbed(ReagentPrototype reagent) : this()
+ {
+ GenerateControl(reagent);
+ }
+
+ public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control)
+ {
+ control = null;
+ if (!args.TryGetValue("Reagent", out var id))
+ {
+ Logger.Error("Reagent embed tag is missing reagent prototype argument");
+ return false;
+ }
+
+ if (!_prototype.TryIndex(id, out var reagent))
+ {
+ Logger.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype");
+ return false;
+ }
+
+ GenerateControl(reagent);
+
+ control = this;
+ return true;
+ }
+
+ private void GenerateControl(ReagentPrototype reagent)
+ {
+ NameBackground.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = reagent.SubstanceColor
+ };
+
+ var textColor = Color.ToHsl(reagent.SubstanceColor).Z > 0.45
+ ? Color.Black
+ : Color.White;
+
+ ReagentName.SetMarkup(Loc.GetString("guidebook-reagent-name",
+ ("color", textColor), ("name", reagent.LocalizedName)));
+
+ #region Recipe
+ // by default, we assume that the reaction has the same ID as the reagent.
+ // if this isn't true, we'll loop through reactions.
+ if (!_prototype.TryIndex(reagent.ID, out var reactionPrototype))
+ {
+ reactionPrototype = _prototype.EnumeratePrototypes()
+ .FirstOrDefault(p => p.Products.ContainsKey(reagent.ID));
+ }
+
+ if (reactionPrototype != null)
+ {
+ var reactantMsg = new FormattedMessage();
+ var reactantsCount = reactionPrototype.Reactants.Count;
+ var i = 0;
+ foreach (var (product, reactant) in reactionPrototype.Reactants)
+ {
+ reactantMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display",
+ ("reagent", _prototype.Index(product).LocalizedName), ("ratio", reactant.Amount)));
+ i++;
+ if (i < reactantsCount)
+ reactantMsg.PushNewline();
+ }
+ reactantMsg.Pop();
+ ReactantsLabel.SetMessage(reactantMsg);
+
+ var productMsg = new FormattedMessage();
+ var productCount = reactionPrototype.Products.Count;
+ var u = 0;
+ foreach (var (product, ratio) in reactionPrototype.Products)
+ {
+ productMsg.AddMarkup(Loc.GetString("guidebook-reagent-recipes-reagent-display",
+ ("reagent", _prototype.Index(product).LocalizedName), ("ratio", ratio)));
+ u++;
+ if (u < productCount)
+ productMsg.PushNewline();
+ }
+ productMsg.Pop();
+ ProductsLabel.SetMessage(productMsg);
+ }
+ else
+ {
+ RecipesContainer.Visible = false;
+ }
+ #endregion
+
+ #region Effects
+ if (_chemistryGuideData.ReagentGuideRegistry.TryGetValue(reagent.ID, out var guideEntryRegistry) &&
+ guideEntryRegistry.GuideEntries != null &&
+ guideEntryRegistry.GuideEntries.Values.Any(pair => pair.EffectDescriptions.Any()))
+ {
+ EffectsDescriptionContainer.Children.Clear();
+ foreach (var (group, effect) in guideEntryRegistry.GuideEntries)
+ {
+ if (!effect.EffectDescriptions.Any())
+ continue;
+
+ var groupLabel = new RichTextLabel();
+ groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-group-rate",
+ ("group", group), ("rate", effect.MetabolismRate)));
+ var descriptionLabel = new RichTextLabel
+ {
+ Margin = new Thickness(25, 0, 10, 0)
+ };
+
+ var descMsg = new FormattedMessage();
+ var descriptionsCount = effect.EffectDescriptions.Length;
+ var i = 0;
+ foreach (var effectString in effect.EffectDescriptions)
+ {
+ descMsg.AddMarkup(effectString);
+ i++;
+ if (i < descriptionsCount)
+ descMsg.PushNewline();
+ }
+ descriptionLabel.SetMessage(descMsg);
+
+ EffectsDescriptionContainer.AddChild(groupLabel);
+ EffectsDescriptionContainer.AddChild(descriptionLabel);
+ }
+ }
+ else
+ {
+ EffectsContainer.Visible = false;
+ }
+ #endregion
+
+ FormattedMessage description = new();
+ description.AddText(reagent.LocalizedDescription);
+ description.PushNewline();
+ description.AddText(Loc.GetString("guidebook-reagent-physical-description",
+ ("description", reagent.LocalizedPhysicalDescription)));
+ ReagentDescription.SetMessage(description);
+ }
+}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml
new file mode 100644
index 0000000000..da671adaa7
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs
new file mode 100644
index 0000000000..0c9356eccb
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Client.Guidebook.Richtext;
+using Content.Shared.Chemistry.Reagent;
+using JetBrains.Annotations;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Guidebook.Controls;
+
+///
+/// Control for embedding a reagent into a guidebook.
+///
+[UsedImplicitly, GenerateTypedNameReferences]
+public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public GuideReagentGroupEmbed()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ MouseFilter = MouseFilterMode.Stop;
+ }
+
+ public GuideReagentGroupEmbed(string group) : this()
+ {
+ var prototypes = _prototype.EnumeratePrototypes()
+ .Where(p => p.Group.Equals(group)).OrderBy(p => p.LocalizedName);
+ foreach (var reagent in prototypes)
+ {
+ var embed = new GuideReagentEmbed(reagent);
+ GroupContainer.AddChild(embed);
+ }
+ }
+
+ public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control)
+ {
+ control = null;
+ if (!args.TryGetValue("Group", out var group))
+ {
+ Logger.Error("Reagent group embed tag is missing group argument");
+ return false;
+ }
+
+ var prototypes = _prototype.EnumeratePrototypes()
+ .Where(p => p.Group.Equals(group)).OrderBy(p => p.LocalizedName);
+ foreach (var reagent in prototypes)
+ {
+ var embed = new GuideReagentEmbed(reagent);
+ GroupContainer.AddChild(embed);
+ }
+
+ control = this;
+ return true;
+ }
+}
diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
index 32a7e2b070..61054e9b0f 100644
--- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
@@ -1,7 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls.FancyTree;
+using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.ContentPack;
@@ -9,7 +13,7 @@
namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences]
-public sealed partial class GuidebookWindow : FancyWindow
+public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
@@ -139,4 +143,20 @@ private void RepopulateTree(List? roots = null, string? forcedRoot = nul
return item;
}
+
+ public void HandleClick(string link)
+ {
+ if (!_entries.TryGetValue(link, out var entry))
+ return;
+
+ if (Tree.TryGetIndexFromMetadata(entry, out var index))
+ {
+ Tree.ExpandParentEntries(index.Value);
+ Tree.SetSelectedIndex(index);
+ }
+ else
+ {
+ ShowGuide(entry);
+ }
+ }
}
diff --git a/Content.Client/Guidebook/GuidebookSystem.cs b/Content.Client/Guidebook/GuidebookSystem.cs
index acaa51524b..6ed38ddef0 100644
--- a/Content.Client/Guidebook/GuidebookSystem.cs
+++ b/Content.Client/Guidebook/GuidebookSystem.cs
@@ -9,7 +9,9 @@
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.Player;
+using Robust.Shared.Map;
using Robust.Shared.Player;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Guidebook;
@@ -19,6 +21,7 @@ namespace Content.Client.Guidebook;
///
public sealed class GuidebookSystem : EntitySystem
{
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly VerbSystem _verbSystem = default!;
@@ -29,6 +32,8 @@ public sealed class GuidebookSystem : EntitySystem
public event Action, List?, string?, bool, string?>? OnGuidebookOpen;
public const string GuideEmbedTag = "GuideEmbeded";
+ private EntityUid _defaultUser;
+
///
public override void Initialize()
{
@@ -41,6 +46,23 @@ public override void Initialize()
OnGuidebookControlsTestGetAlternateVerbs);
}
+ ///
+ /// Gets a user entity to use for verbs and examinations. If the player has no attached entity, this will use a
+ /// dummy client-side entity so that users can still use the guidebook when not attached to anything (e.g., in the
+ /// lobby)
+ ///
+ public EntityUid GetGuidebookUser()
+ {
+ var user = _playerManager.LocalPlayer!.ControlledEntity;
+ if (user != null)
+ return user.Value;
+
+ if (!Exists(_defaultUser))
+ _defaultUser = Spawn(null, MapCoordinates.Nullspace);
+
+ return _defaultUser;
+ }
+
private void OnGetVerbs(EntityUid uid, GuideHelpComponent component, GetVerbsEvent args)
{
if (component.Guides.Count == 0 || _tags.HasTag(uid, GuideEmbedTag))
@@ -58,6 +80,9 @@ private void OnGetVerbs(EntityUid uid, GuideHelpComponent component, GetVerbsEve
private void OnInteract(EntityUid uid, GuideHelpComponent component, ActivateInWorldEvent args)
{
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
if (!component.OpenOnActivation || component.Guides.Count == 0 || _tags.HasTag(uid, GuideEmbedTag))
return;
@@ -114,34 +139,26 @@ private void OnGuidebookControlsTestInteractHand(EntityUid uid, GuidebookControl
_audioSystem.PlayGlobal(speech.SpeechSounds, Filter.Local(), false, speech.AudioParams);
}
-
public void FakeClientActivateInWorld(EntityUid activated)
{
- var user = _playerManager.LocalPlayer!.ControlledEntity;
- if (user is null)
- return;
- var activateMsg = new ActivateInWorldEvent(user.Value, activated);
- RaiseLocalEvent(activated, activateMsg, true);
+ var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated);
+ RaiseLocalEvent(activated, activateMsg);
}
public void FakeClientAltActivateInWorld(EntityUid activated)
{
- var user = _playerManager.LocalPlayer!.ControlledEntity;
- if (user is null)
- return;
// Get list of alt-interact verbs
- var verbs = _verbSystem.GetLocalVerbs(activated, user.Value, typeof(AlternativeVerb));
+ var verbs = _verbSystem.GetLocalVerbs(activated, GetGuidebookUser(), typeof(AlternativeVerb), force: true);
if (!verbs.Any())
return;
- _verbSystem.ExecuteVerb(verbs.First(), user.Value, activated);
+ _verbSystem.ExecuteVerb(verbs.First(), GetGuidebookUser(), activated);
}
public void FakeClientUse(EntityUid activated)
{
- var user = _playerManager.LocalPlayer!.ControlledEntity ?? EntityUid.Invalid;
- var activateMsg = new InteractHandEvent(user, activated);
- RaiseLocalEvent(activated, activateMsg, true);
+ var activateMsg = new InteractHandEvent(GetGuidebookUser(), activated);
+ RaiseLocalEvent(activated, activateMsg);
}
}
diff --git a/Content.Client/Guidebook/Richtext/TextLinkTag.cs b/Content.Client/Guidebook/Richtext/TextLinkTag.cs
new file mode 100644
index 0000000000..b1e8460bb8
--- /dev/null
+++ b/Content.Client/Guidebook/Richtext/TextLinkTag.cs
@@ -0,0 +1,70 @@
+using System.Diagnostics.CodeAnalysis;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Input;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Guidebook.RichText;
+
+[UsedImplicitly]
+public sealed class TextLinkTag : IMarkupTag
+{
+ public string Name => "textlink";
+
+ public Control? Control;
+
+ ///
+ public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
+ {
+ if (!node.Value.TryGetString(out var text)
+ || !node.Attributes.TryGetValue("link", out var linkParameter)
+ || !linkParameter.TryGetString(out var link))
+ {
+ control = null;
+ return false;
+ }
+
+ var label = new Label();
+ label.Text = text;
+
+ label.MouseFilter = Control.MouseFilterMode.Stop;
+ label.FontColorOverride = Color.CornflowerBlue;
+ label.DefaultCursorShape = Control.CursorShape.Hand;
+
+ label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue;
+ label.OnMouseExited += _ => label.FontColorOverride = Color.CornflowerBlue;
+ label.OnKeyBindDown += args => OnKeybindDown(args, link);
+
+ control = label;
+ Control = label;
+ return true;
+ }
+
+ private void OnKeybindDown(GUIBoundKeyEventArgs args, string link)
+ {
+ if (args.Function != EngineKeyFunctions.UIClick)
+ return;
+
+ if (Control == null)
+ return;
+
+ var current = Control;
+ while (current != null)
+ {
+ current = current.Parent;
+
+ if (current is not ILinkClickHandler handler)
+ continue;
+ handler.HandleClick(link);
+ return;
+ }
+ Logger.Warning($"Warning! No valid ILinkClickHandler found.");
+ }
+}
+
+public interface ILinkClickHandler
+{
+ public void HandleClick(string link);
+}
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index c7d45ec085..b98b907cd0 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -18,6 +18,7 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Module;
using Content.Client.Guidebook;
+using Content.Client.Replay;
using Content.Shared.Administration.Managers;
namespace Content.Client.IoC
@@ -44,6 +45,7 @@ public static void Register()
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
}
}
}
diff --git a/Content.Client/MainMenu/UI/MainMenuControl.xaml.cs b/Content.Client/MainMenu/UI/MainMenuControl.xaml.cs
index 5216547b92..e9e90288c6 100644
--- a/Content.Client/MainMenu/UI/MainMenuControl.xaml.cs
+++ b/Content.Client/MainMenu/UI/MainMenuControl.xaml.cs
@@ -1,13 +1,10 @@
-using Content.Client.Changelog;
-using Content.Client.Parallax;
-using Robust.Client.AutoGenerated;
+using Robust.Client.AutoGenerated;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
-using Robust.Shared.Localization;
namespace Content.Client.MainMenu.UI
{
diff --git a/Content.Client/PDA/PDAMenu.xaml.cs b/Content.Client/PDA/PDAMenu.xaml.cs
index 4b80795cc1..a639c9d184 100644
--- a/Content.Client/PDA/PDAMenu.xaml.cs
+++ b/Content.Client/PDA/PDAMenu.xaml.cs
@@ -37,11 +37,11 @@ public PDAMenu()
ViewContainer.OnChildAdded += control => control.Visible = false;
- HomeButton.IconTexture = new SpriteSpecifier.Texture(new ("/Textures/Interface/home.png"));
- FlashLightToggleButton.IconTexture = new SpriteSpecifier.Texture(new ("/Textures/Interface/light.png"));
- EjectPenButton.IconTexture = new SpriteSpecifier.Texture(new ("/Textures/Interface/pencil.png"));
- EjectIdButton.IconTexture = new SpriteSpecifier.Texture(new ("/Textures/Interface/eject.png"));
- ProgramCloseButton.IconTexture = new SpriteSpecifier.Texture(new ("/Textures/Interface/Nano/cross.svg.png"));
+ HomeButton.IconTexture = new SpriteSpecifier.Texture(new("/Textures/Interface/home.png"));
+ FlashLightToggleButton.IconTexture = new SpriteSpecifier.Texture(new("/Textures/Interface/light.png"));
+ EjectPenButton.IconTexture = new SpriteSpecifier.Texture(new("/Textures/Interface/pencil.png"));
+ EjectIdButton.IconTexture = new SpriteSpecifier.Texture(new("/Textures/Interface/eject.png"));
+ ProgramCloseButton.IconTexture = new SpriteSpecifier.Texture(new("/Textures/Interface/Nano/cross.svg.png"));
HomeButton.OnPressed += _ => ToHomeScreen();
@@ -102,8 +102,8 @@ public void UpdateState(PDAUpdateState state)
if (state.PDAOwnerInfo.IdOwner != null || state.PDAOwnerInfo.JobTitle != null)
{
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
- ("owner",state.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
- ("jobTitle",state.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
+ ("owner", state.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
+ ("jobTitle", state.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
}
else
{
@@ -111,7 +111,7 @@ public void UpdateState(PDAUpdateState state)
}
StationNameLabel.SetMarkup(Loc.GetString("comp-pda-ui-station",
- ("station",state.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
+ ("station", state.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
@@ -263,7 +263,7 @@ public void ChangeView(int view)
_currentView = view;
}
- private BoxContainer CreateProgramListRow()
+ private static BoxContainer CreateProgramListRow()
{
return new BoxContainer()
{
diff --git a/Content.Client/PDA/PDAProgramItem.xaml.cs b/Content.Client/PDA/PDAProgramItem.xaml.cs
index 5cc16da497..9348cc5c12 100644
--- a/Content.Client/PDA/PDAProgramItem.xaml.cs
+++ b/Content.Client/PDA/PDAProgramItem.xaml.cs
@@ -1,9 +1,7 @@
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.Input;
namespace Content.Client.PDA;
diff --git a/Content.Client/PDA/PDASettingsButton.xaml.cs b/Content.Client/PDA/PDASettingsButton.xaml.cs
index 0fb4ffb596..c62f214194 100644
--- a/Content.Client/PDA/PDASettingsButton.xaml.cs
+++ b/Content.Client/PDA/PDASettingsButton.xaml.cs
@@ -42,7 +42,7 @@ public Color? ForegroundColor
{
get => OptionName.FontColorOverride;
- set
+ set
{
OptionName.FontColorOverride = value;
OptionDescription.FontColorOverride = value;
diff --git a/Content.Client/PDA/PDASystem.cs b/Content.Client/PDA/PDASystem.cs
index 37834e0723..a5de98b9cf 100644
--- a/Content.Client/PDA/PDASystem.cs
+++ b/Content.Client/PDA/PDASystem.cs
@@ -18,10 +18,10 @@ private void OnAppearanceChange(EntityUid uid, PDAComponent component, ref Appea
if (args.Sprite == null)
return;
- if (_appearance.TryGetData(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
+ if (Appearance.TryGetData(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
args.Sprite.LayerSetVisible(PDAVisualLayers.Flashlight, isFlashlightOn);
- if (_appearance.TryGetData(uid, PDAVisuals.IDCardInserted, out var isCardInserted, args.Component))
+ if (Appearance.TryGetData(uid, PDAVisuals.IDCardInserted, out var isCardInserted, args.Component))
args.Sprite.LayerSetVisible(PDAVisualLayers.IDLight, isCardInserted);
}
@@ -29,7 +29,7 @@ protected override void OnComponentInit(EntityUid uid, PDAComponent component, C
{
base.OnComponentInit(uid, component, args);
- if(!TryComp(uid, out var sprite))
+ if (!TryComp(uid, out var sprite))
return;
if (component.State != null)
@@ -38,11 +38,11 @@ protected override void OnComponentInit(EntityUid uid, PDAComponent component, C
sprite.LayerSetVisible(PDAVisualLayers.Flashlight, component.FlashlightOn);
sprite.LayerSetVisible(PDAVisualLayers.IDLight, component.IdSlot.StartingItem != null);
}
-}
-enum PDAVisualLayers : byte
-{
- Base,
- Flashlight,
- IDLight
+ public enum PDAVisualLayers : byte
+ {
+ Base,
+ Flashlight,
+ IDLight
+ }
}
diff --git a/Content.Client/PDA/PDAWindow.xaml.cs b/Content.Client/PDA/PDAWindow.xaml.cs
index 8b39d744cd..4a7ef89c64 100644
--- a/Content.Client/PDA/PDAWindow.xaml.cs
+++ b/Content.Client/PDA/PDAWindow.xaml.cs
@@ -22,7 +22,7 @@ public string? AccentHColor
set
{
- AccentH.ModulateSelfOverride = Color. FromHex(value, Color.White);
+ AccentH.ModulateSelfOverride = Color.FromHex(value, Color.White);
AccentH.Visible = value != null;
}
}
@@ -33,7 +33,7 @@ public string? AccentVColor
set
{
- AccentV.ModulateSelfOverride = Color. FromHex(value, Color.White);
+ AccentV.ModulateSelfOverride = Color.FromHex(value, Color.White);
AccentV.Visible = value != null;
}
}
diff --git a/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs b/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs
index cb638efcd9..1363fa66d1 100644
--- a/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs
+++ b/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs
@@ -18,20 +18,38 @@ public RingtoneMenu()
RingerNoteInputs = new[] { RingerNoteOneInput, RingerNoteTwoInput, RingerNoteThreeInput, RingerNoteFourInput };
- for (int i = 0; i < RingerNoteInputs.Length; i++)
+ for (var i = 0; i < RingerNoteInputs.Length; ++i)
{
var input = RingerNoteInputs[i];
- int index = i;
- input.OnTextChanged += _ => //Prevents unauthorized characters from being entered into the LineEdit
+ var index = i;
+ var foo = () => // Prevents unauthorized characters from being entered into the LineEdit
{
input.Text = input.Text.ToUpper();
if (!IsNote(input.Text))
+ {
input.Text = PreviousNoteInputs[index];
+ }
else
PreviousNoteInputs[index] = input.Text;
- input.CursorPosition = input.Text.Length; //Resets caret position to the end of the typed input
+ input.RemoveStyleClass("Caution");
+ };
+
+ input.OnFocusExit += _ => foo();
+ input.OnTextEntered += _ =>
+ {
+ foo();
+ input.CursorPosition = input.Text.Length; // Resets caret position to the end of the typed input
+ };
+ input.OnTextChanged += _ =>
+ {
+ input.Text = input.Text.ToUpper();
+
+ if (!IsNote(input.Text))
+ input.AddStyleClass("Caution");
+ else
+ input.RemoveStyleClass("Caution");
};
}
}
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs
index c41f67173d..734def3653 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorBoundUserInterface.cs
@@ -1,6 +1,5 @@
using Content.Shared.Singularity.Components;
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
namespace Content.Client.ParticleAccelerator.UI
{
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
index 0b916c7600..0df9363cb8 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
@@ -19,10 +19,10 @@ public sealed class ParticleAcceleratorControlMenu : BaseWindow
{
private readonly ShaderInstance _greyScaleShader;
- private readonly ParticleAcceleratorBoundUserInterface Owner;
+ private readonly ParticleAcceleratorBoundUserInterface _owner;
private readonly Label _drawLabel;
- private readonly NoiseGenerator _drawNoiseGenerator;
+ private readonly FastNoiseLite _drawNoiseGenerator;
private readonly Button _onButton;
private readonly Button _offButton;
private readonly Button _scanButton;
@@ -36,9 +36,9 @@ public sealed class ParticleAcceleratorControlMenu : BaseWindow
private readonly PASegmentControl _fuelChamberTexture;
private readonly PASegmentControl _controlBoxTexture;
private readonly PASegmentControl _powerBoxTexture;
- private readonly PASegmentControl _emitterCenterTexture;
- private readonly PASegmentControl _emitterRightTexture;
- private readonly PASegmentControl _emitterLeftTexture;
+ private readonly PASegmentControl _emitterForeTexture;
+ private readonly PASegmentControl _emitterPortTexture;
+ private readonly PASegmentControl _emitterStarboardTexture;
private float _time;
private int _lastDraw;
@@ -47,14 +47,16 @@ public sealed class ParticleAcceleratorControlMenu : BaseWindow
private bool _blockSpinBox;
private bool _assembled;
private bool _shouldContinueAnimating;
+ private int _maxStrength = 3;
public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owner)
{
SetSize = (400, 320);
_greyScaleShader = IoCManager.Resolve().Index("Greyscale").Instance();
- Owner = owner;
- _drawNoiseGenerator = new NoiseGenerator(NoiseGenerator.NoiseType.Fbm);
+ _owner = owner;
+ _drawNoiseGenerator = new();
+ _drawNoiseGenerator.SetFractalType(FastNoiseLite.FractalType.FBm);
_drawNoiseGenerator.SetFrequency(0.5f);
var resourceCache = IoCManager.Resolve();
@@ -98,7 +100,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
MouseFilter = MouseFilterMode.Pass
});
- _stateSpinBox = new SpinBox {Value = 0, IsValid = StrengthSpinBoxValid,};
+ _stateSpinBox = new SpinBox { Value = 0, IsValid = StrengthSpinBoxValid };
_stateSpinBox.InitDefaultButtons();
_stateSpinBox.ValueChanged += PowerStateChanged;
_stateSpinBox.LineEditDisabled = true;
@@ -107,7 +109,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
{
ToggleMode = false,
Text = Loc.GetString("particle-accelerator-control-menu-off-button"),
- StyleClasses = {StyleBase.ButtonOpenRight},
+ StyleClasses = { StyleBase.ButtonOpenRight },
};
_offButton.OnPressed += args => owner.SendEnableMessage(false);
@@ -115,13 +117,13 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
{
ToggleMode = false,
Text = Loc.GetString("particle-accelerator-control-menu-on-button"),
- StyleClasses = {StyleBase.ButtonOpenLeft},
+ StyleClasses = { StyleBase.ButtonOpenLeft },
};
_onButton.OnPressed += args => owner.SendEnableMessage(true);
var closeButton = new TextureButton
{
- StyleClasses = {"windowCloseButton"},
+ StyleClasses = { "windowCloseButton" },
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, 8, 0)
};
@@ -130,7 +132,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
var serviceManual = new Label
{
HorizontalAlignment = HAlignment.Center,
- StyleClasses = {StyleBase.StyleClassLabelSubText},
+ StyleClasses = { StyleBase.StyleClassLabelSubText },
Text = Loc.GetString("particle-accelerator-control-menu-service-manual-reference")
};
_drawLabel = new Label();
@@ -227,7 +229,8 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
Align = Label.AlignMode.Center
},
serviceManual
- }
+ },
+ Visible = false,
}),
}
},
@@ -267,9 +270,9 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
new Control {MinSize = imgSize},
(_powerBoxTexture = Segment("power_box")),
new Control {MinSize = imgSize},
- (_emitterLeftTexture = Segment("emitter_left")),
- (_emitterCenterTexture = Segment("emitter_center")),
- (_emitterRightTexture = Segment("emitter_right")),
+ (_emitterStarboardTexture = Segment("emitter_starboard")),
+ (_emitterForeTexture = Segment("emitter_fore")),
+ (_emitterPortTexture = Segment("emitter_port")),
}
}
}
@@ -312,7 +315,7 @@ public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owne
}
});
- _scanButton.OnPressed += args => Owner.SendScanPartsMessage();
+ _scanButton.OnPressed += args => _owner.SendScanPartsMessage();
_alarmControl.AnimationCompleted += s =>
{
@@ -336,7 +339,7 @@ PASegmentControl Segment(string name)
private bool StrengthSpinBoxValid(int n)
{
- return (n >= 0 && n <= 3 && !_blockSpinBox);
+ return n >= 0 && n <= _maxStrength && !_blockSpinBox;
}
private void PowerStateChanged(ValueChangedEventArgs e)
@@ -356,16 +359,15 @@ private void PowerStateChanged(ValueChangedEventArgs e)
case 3:
newState = ParticleAcceleratorPowerState.Level2;
break;
- // They can't reach this level anyway and I just want to fix the bugginess for now.
- //case 4:
- // newState = ParticleAcceleratorPowerState.Level3;
- // break;
+ case 4:
+ newState = ParticleAcceleratorPowerState.Level3;
+ break;
default:
return;
}
_stateSpinBox.SetButtonDisabled(true);
- Owner.SendPowerStateMessage(newState);
+ _owner.SendPowerStateMessage(newState);
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
@@ -402,14 +404,15 @@ private void UpdatePowerState(ParticleAcceleratorPowerState state, bool enabled,
});
- _shouldContinueAnimating = false;
- _alarmControl.StopAnimation("warningAnim");
- _alarmControl.Visible = false;
- if (maxState == ParticleAcceleratorPowerState.Level3 && enabled && assembled)
+ _maxStrength = maxState == ParticleAcceleratorPowerState.Level3 ? 4 : 3;
+ if (_maxStrength > 3 && enabled && assembled)
{
_shouldContinueAnimating = true;
- _alarmControl.PlayAnimation(_alarmControlAnimation, "warningAnim");
+ if (!_alarmControl.Visible)
+ _alarmControl.PlayAnimation(_alarmControlAnimation, "warningAnim");
}
+ else
+ _shouldContinueAnimating = false;
}
private void UpdateUI(bool assembled, bool blocked, bool enabled, bool powerBlock)
@@ -430,12 +433,12 @@ private void UpdateUI(bool assembled, bool blocked, bool enabled, bool powerBloc
private void UpdatePreview(ParticleAcceleratorUIState updateMessage)
{
_endCapTexture.SetPowerState(updateMessage, updateMessage.EndCapExists);
- _fuelChamberTexture.SetPowerState(updateMessage, updateMessage.FuelChamberExists);
_controlBoxTexture.SetPowerState(updateMessage, true);
+ _fuelChamberTexture.SetPowerState(updateMessage, updateMessage.FuelChamberExists);
_powerBoxTexture.SetPowerState(updateMessage, updateMessage.PowerBoxExists);
- _emitterCenterTexture.SetPowerState(updateMessage, updateMessage.EmitterCenterExists);
- _emitterLeftTexture.SetPowerState(updateMessage, updateMessage.EmitterLeftExists);
- _emitterRightTexture.SetPowerState(updateMessage, updateMessage.EmitterRightExists);
+ _emitterStarboardTexture.SetPowerState(updateMessage, updateMessage.EmitterStarboardExists);
+ _emitterForeTexture.SetPowerState(updateMessage, updateMessage.EmitterForeExists);
+ _emitterPortTexture.SetPowerState(updateMessage, updateMessage.EmitterPortExists);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -453,7 +456,7 @@ protected override void FrameUpdate(FrameEventArgs args)
var watts = 0;
if (_lastDraw != 0)
{
- var val = _drawNoiseGenerator.GetNoise(_time);
+ var val = _drawNoiseGenerator.GetNoise(_time, 0f);
watts = (int) (_lastDraw + val * 5);
}
@@ -476,7 +479,7 @@ public PASegmentControl(ParticleAcceleratorControlMenu menu, IResourceCache cach
_baseState = name;
_rsi = cache.GetResource($"/Textures/Structures/Power/Generation/PA/{name}.rsi").RSI;
- AddChild(_base = new TextureRect {Texture = _rsi[$"completed"].Frame0});
+ AddChild(_base = new TextureRect { Texture = _rsi[$"completed"].Frame0 });
AddChild(_unlit = new TextureRect());
MinSize = _rsi.Size;
}
diff --git a/Content.Client/Replay/ContentReplayPlaybackManager.cs b/Content.Client/Replay/ContentReplayPlaybackManager.cs
new file mode 100644
index 0000000000..0ecb98ec0d
--- /dev/null
+++ b/Content.Client/Replay/ContentReplayPlaybackManager.cs
@@ -0,0 +1,143 @@
+using Content.Client.Administration.Managers;
+using Content.Client.Launcher;
+using Content.Client.MainMenu;
+using Content.Client.Replay.UI.Loading;
+using Content.Client.UserInterface.Systems.Chat;
+using Content.Shared.Chat;
+using Content.Shared.GameTicking;
+using Content.Shared.Hands;
+using Content.Shared.Instruments;
+using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Client;
+using Robust.Client.Console;
+using Robust.Client.GameObjects;
+using Robust.Client.Replays.Loading;
+using Robust.Client.Replays.Playback;
+using Robust.Client.State;
+using Robust.Client.Timing;
+using Robust.Client.UserInterface;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay;
+
+public sealed class ContentReplayPlaybackManager
+{
+ [Dependency] private readonly IStateManager _stateMan = default!;
+ [Dependency] private readonly IClientGameTiming _timing = default!;
+ [Dependency] private readonly IReplayLoadManager _loadMan = default!;
+ [Dependency] private readonly IGameController _controller = default!;
+ [Dependency] private readonly IClientEntityManager _entMan = default!;
+ [Dependency] private readonly IUserInterfaceManager _uiMan = default!;
+ [Dependency] private readonly IReplayPlaybackManager _playback = default!;
+ [Dependency] private readonly IClientConGroupController _conGrp = default!;
+ [Dependency] private readonly IClientAdminManager _adminMan = default!;
+
+ ///
+ /// UI state to return to when stopping a replay or loading fails.
+ ///
+ public Type? DefaultState;
+
+ private bool _initialized;
+
+ public void Initialize()
+ {
+ if (_initialized)
+ return;
+
+ _initialized = true;
+ _playback.HandleReplayMessage += OnHandleReplayMessage;
+ _playback.ReplayPlaybackStopped += OnReplayPlaybackStopped;
+ _playback.ReplayPlaybackStarted += OnReplayPlaybackStarted;
+ _playback.ReplayCheckpointReset += OnCheckpointReset;
+ _loadMan.LoadOverride += LoadOverride;
+ }
+
+ private void LoadOverride(IWritableDirProvider dir, ResPath resPath)
+ {
+ var screen = _stateMan.RequestStateChange>();
+ screen.Job = new ContentLoadReplayJob(1/60f, dir, resPath, _loadMan, screen);
+ screen.OnJobFinished += (_, e) => OnFinishedLoading(e);
+ }
+
+ private void OnFinishedLoading(Exception? exception)
+ {
+ if (exception != null)
+ {
+ ReturnToDefaultState();
+ _uiMan.Popup(Loc.GetString("replay-loading-failed", ("reason", exception)));
+ }
+ }
+
+ public void ReturnToDefaultState()
+ {
+ if (DefaultState != null)
+ _stateMan.RequestStateChange(DefaultState);
+ else if (_controller.LaunchState.FromLauncher)
+ _stateMan.RequestStateChange().SetDisconnected();
+ else
+ _stateMan.RequestStateChange();
+ }
+
+ private void OnCheckpointReset()
+ {
+ // This function removes future chat messages when rewinding time.
+
+ // TODO REPLAYS add chat messages when jumping forward in time.
+ // Need to allow content to add data to checkpoint states.
+
+ _uiMan.GetUIController().History.RemoveAll(x => x.Item1 > _timing.CurTick);
+ _uiMan.GetUIController().Repopulate();
+ }
+
+ private bool OnHandleReplayMessage(object message, bool skipEffects)
+ {
+ switch (message)
+ {
+ case ChatMessage chat:
+ // Just pass on the chat message to the UI controller, but skip speech-bubbles if we are fast-forwarding.
+ _uiMan.GetUIController().ProcessChatMessage(chat, speechBubble: !skipEffects);
+ return true;
+ // TODO REPLAYS figure out a cleaner way of doing this. This sucks.
+ // Next: we want to avoid spamming animations, sounds, and pop-ups while scrubbing or rewinding time
+ // (e.g., to rewind 1 tick, we really rewind ~60 and then fast forward 59). Currently, this is
+ // effectively an EntityEvent blacklist. But this is kinda shit and should be done differently somehow.
+ // The unifying aspect of these events is that they trigger pop-ups, UI changes, spawn client-side
+ // entities or start animations.
+ case RoundEndMessageEvent:
+ case PopupEvent:
+ case AudioMessage:
+ case PickupAnimationEvent:
+ case MeleeLungeEvent:
+ case SharedGunSystem.HitscanEvent:
+ case ImpactEffectEvent:
+ case MuzzleFlashEvent:
+ case DamageEffectEvent:
+ case InstrumentStartMidiEvent:
+ case InstrumentMidiEventEvent:
+ case InstrumentStopMidiEvent:
+ if (!skipEffects)
+ _entMan.DispatchReceivedNetworkMsg((EntityEventArgs)message);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ private void OnReplayPlaybackStarted()
+ {
+ _conGrp.Implementation = new ReplayConGroup();
+ }
+
+ private void OnReplayPlaybackStopped()
+ {
+ _conGrp.Implementation = (IClientConGroupImplementation)_adminMan;
+ ReturnToDefaultState();
+ }
+}
diff --git a/Content.Client/Replay/LoadReplayJob.cs b/Content.Client/Replay/LoadReplayJob.cs
new file mode 100644
index 0000000000..1064008609
--- /dev/null
+++ b/Content.Client/Replay/LoadReplayJob.cs
@@ -0,0 +1,39 @@
+using System.Threading.Tasks;
+using Content.Client.Replay.UI.Loading;
+using Robust.Client.Replays.Loading;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay;
+
+public sealed class ContentLoadReplayJob : LoadReplayJob
+{
+ private readonly LoadingScreen _screen;
+
+ public ContentLoadReplayJob(
+ float maxTime,
+ IWritableDirProvider dir,
+ ResPath path,
+ IReplayLoadManager loadMan,
+ LoadingScreen screen)
+ : base(maxTime, dir, path, loadMan)
+ {
+ _screen = screen;
+ }
+
+ protected override async Task Yield(float value, float maxValue, LoadingState state, bool force)
+ {
+ var header = Loc.GetString("replay-loading", ("cur", (int)state + 1), ("total", 5));
+ var subText = Loc.GetString(state switch
+ {
+ LoadingState.ReadingFiles => "replay-loading-reading",
+ LoadingState.ProcessingFiles => "replay-loading-processing",
+ LoadingState.Spawning => "replay-loading-spawning",
+ LoadingState.Initializing => "replay-loading-initializing",
+ _ => "replay-loading-starting",
+ });
+ _screen.UpdateProgress(value, maxValue, header, subText);
+
+ await base.Yield(value, maxValue, state, force);
+ }
+}
diff --git a/Content.Client/Replay/ReplayConGroup.cs b/Content.Client/Replay/ReplayConGroup.cs
new file mode 100644
index 0000000000..8e85632683
--- /dev/null
+++ b/Content.Client/Replay/ReplayConGroup.cs
@@ -0,0 +1,13 @@
+using Robust.Client.Console;
+
+namespace Content.Client.Replay;
+
+public sealed class ReplayConGroup : IClientConGroupImplementation
+{
+ public event Action? ConGroupUpdated;
+ public bool CanAdminMenu() => true;
+ public bool CanAdminPlace() => true;
+ public bool CanCommand(string cmdName) => true;
+ public bool CanScript() => true;
+ public bool CanViewVar() => true;
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorComponent.cs b/Content.Client/Replay/Spectator/ReplaySpectatorComponent.cs
new file mode 100644
index 0000000000..509240a386
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Client.Replay.Spectator;
+
+///
+/// This component indicates that this entity currently has a replay spectator/observer attached to it.
+///
+[RegisterComponent]
+public sealed class ReplaySpectatorComponent : Component
+{
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs
new file mode 100644
index 0000000000..86d113defb
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Blockers.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Hands;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory.Events;
+using Content.Shared.Item;
+using Content.Shared.Movement.Events;
+using Content.Shared.Physics.Pull;
+using Content.Shared.Throwing;
+
+namespace Content.Client.Replay.Spectator;
+
+public sealed partial class ReplaySpectatorSystem
+{
+ private void InitializeBlockers()
+ {
+ // Block most interactions to avoid mispredicts
+ // This **shouldn't** be required, but just in case.
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnAttempt);
+ SubscribeLocalEvent(OnUpdateCanMove);
+ SubscribeLocalEvent(OnUpdateCanMove);
+ SubscribeLocalEvent(OnPullAttempt);
+ }
+
+ private void OnAttempt(EntityUid uid, ReplaySpectatorComponent component, CancellableEntityEventArgs args)
+ {
+ args.Cancel();
+ }
+
+ private void OnUpdateCanMove(EntityUid uid, ReplaySpectatorComponent component, CancellableEntityEventArgs args)
+ {
+ args.Cancel();
+ }
+
+ private void OnPullAttempt(EntityUid uid, ReplaySpectatorComponent component, PullAttemptEvent args)
+ {
+ args.Cancelled = true;
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs
new file mode 100644
index 0000000000..52cf3d9c3b
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs
@@ -0,0 +1,123 @@
+using Content.Shared.Movement.Components;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
+using Robust.Shared.Players;
+
+namespace Content.Client.Replay.Spectator;
+
+// Partial class handles movement logic for observers.
+public sealed partial class ReplaySpectatorSystem
+{
+ public DirectionFlag Direction;
+
+ ///
+ /// Fallback speed if the observer ghost has no .
+ ///
+ public const float DefaultSpeed = 12;
+
+ private void InitializeMovement()
+ {
+ var moveUpCmdHandler = new MoverHandler(this, DirectionFlag.North);
+ var moveLeftCmdHandler = new MoverHandler(this, DirectionFlag.West);
+ var moveRightCmdHandler = new MoverHandler(this, DirectionFlag.East);
+ var moveDownCmdHandler = new MoverHandler(this, DirectionFlag.South);
+
+ CommandBinds.Builder
+ .Bind(EngineKeyFunctions.MoveUp, moveUpCmdHandler)
+ .Bind(EngineKeyFunctions.MoveLeft, moveLeftCmdHandler)
+ .Bind(EngineKeyFunctions.MoveRight, moveRightCmdHandler)
+ .Bind(EngineKeyFunctions.MoveDown, moveDownCmdHandler)
+ .Register();
+ }
+
+ private void ShutdownMovement()
+ {
+ CommandBinds.Unregister();
+ }
+
+ // Normal mover code works via physics. Replays don't do prediction/physics. You can fudge it by relying on the
+ // fact that only local-player physics is currently predicted, but instead I've just added crude mover logic here.
+ // This just runs on frame updates, no acceleration or friction here.
+ public override void FrameUpdate(float frameTime)
+ {
+ if (_replayPlayback.Replay == null)
+ return;
+
+ if (_player.LocalPlayer?.ControlledEntity is not { } player)
+ return;
+
+ if (Direction == DirectionFlag.None)
+ {
+ if (TryComp(player, out InputMoverComponent? cmp))
+ _mover.LerpRotation(cmp, frameTime);
+ return;
+ }
+
+ if (!player.IsClientSide() || !HasComp(player))
+ {
+ // Player is trying to move -> behave like the ghost-on-move component.
+ SpawnSpectatorGhost(new EntityCoordinates(player, default), true);
+ return;
+ }
+
+ if (!TryComp(player, out InputMoverComponent? mover))
+ return;
+
+ _mover.LerpRotation(mover, frameTime);
+
+ var effectiveDir = Direction;
+ if ((Direction & DirectionFlag.North) != 0)
+ effectiveDir &= ~DirectionFlag.South;
+
+ if ((Direction & DirectionFlag.East) != 0)
+ effectiveDir &= ~DirectionFlag.West;
+
+ var query = GetEntityQuery();
+ var xform = query.GetComponent(player);
+ var pos = _transform.GetWorldPosition(xform, query);
+
+ if (!xform.ParentUid.IsValid())
+ {
+ // Were they sitting on a grid as it was getting deleted?
+ SetSpectatorPosition(default);
+ return;
+ }
+
+ // A poor mans grid-traversal system. Should also interrupt ghost-following.
+ _transform.SetGridId(player, xform, null);
+ _transform.AttachToGridOrMap(player);
+
+ var parentRotation = _mover.GetParentGridAngle(mover, query);
+ var localVec = effectiveDir.AsDir().ToAngle().ToWorldVec();
+ var worldVec = parentRotation.RotateVec(localVec);
+ var speed = CompOrNull(player)?.BaseSprintSpeed ?? DefaultSpeed;
+ var delta = worldVec * frameTime * speed;
+ _transform.SetWorldPositionRotation(xform, pos + delta, delta.ToWorldAngle(), query);
+ }
+
+ private sealed class MoverHandler : InputCmdHandler
+ {
+ private readonly ReplaySpectatorSystem _sys;
+ private readonly DirectionFlag _dir;
+
+ public MoverHandler(ReplaySpectatorSystem sys, DirectionFlag dir)
+ {
+ _sys = sys;
+ _dir = dir;
+ }
+
+ public override bool HandleCmdMessage(ICommonSession? session, InputCmdMessage message)
+ {
+ if (message is not FullInputCmdMessage full)
+ return false;
+
+ if (full.State == BoundKeyState.Down)
+ _sys.Direction |= _dir;
+ else
+ _sys.Direction &= ~_dir;
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
new file mode 100644
index 0000000000..3b17191c16
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
@@ -0,0 +1,132 @@
+using System.Linq;
+using Content.Shared.Movement.Components;
+using Robust.Client.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.Replay.Spectator;
+
+// This partial class contains functions for getting and setting the spectator's position data, so that
+// a consistent view/camera can be maintained when jumping around in time.
+public sealed partial class ReplaySpectatorSystem
+{
+ ///
+ /// Simple struct containing position & rotation data for maintaining a persistent view when jumping around in time.
+ ///
+ public struct SpectatorPosition
+ {
+ // TODO REPLAYS handle ghost-following.
+ public EntityUid Entity;
+ public (EntityCoordinates Coords, Angle Rot)? Local;
+ public (EntityCoordinates Coords, Angle Rot)? World;
+ public (EntityUid? Ent, Angle Rot)? Eye;
+ }
+
+ public SpectatorPosition GetSpectatorPosition()
+ {
+ var obs = new SpectatorPosition();
+ if (_player.LocalPlayer?.ControlledEntity is { } player && TryComp(player, out TransformComponent? xform) && xform.MapUid != null)
+ {
+ obs.Local = (xform.Coordinates, xform.LocalRotation);
+ obs.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
+
+ if (TryComp(player, out InputMoverComponent? mover))
+ obs.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
+
+ obs.Entity = player;
+ }
+
+ return obs;
+ }
+
+ private void OnBeforeSetTick()
+ {
+ _oldPosition = GetSpectatorPosition();
+ }
+
+ private void OnAfterSetTick()
+ {
+ if (_oldPosition != null)
+ SetSpectatorPosition(_oldPosition.Value);
+ _oldPosition = null;
+ }
+
+ public void SetSpectatorPosition(SpectatorPosition spectatorPosition)
+ {
+ if (Exists(spectatorPosition.Entity) && Transform(spectatorPosition.Entity).MapID != MapId.Nullspace)
+ {
+ _player.LocalPlayer!.AttachEntity(spectatorPosition.Entity, EntityManager, _client);
+ return;
+ }
+
+ if (spectatorPosition.Local != null && spectatorPosition.Local.Value.Coords.IsValid(EntityManager))
+ {
+ var newXform = SpawnSpectatorGhost(spectatorPosition.Local.Value.Coords, false);
+ newXform.LocalRotation = spectatorPosition.Local.Value.Rot;
+ }
+ else if (spectatorPosition.World != null && spectatorPosition.World.Value.Coords.IsValid(EntityManager))
+ {
+ var newXform = SpawnSpectatorGhost(spectatorPosition.World.Value.Coords, true);
+ newXform.LocalRotation = spectatorPosition.World.Value.Rot;
+ }
+ else if (TryFindFallbackSpawn(out var coords))
+ {
+ var newXform = SpawnSpectatorGhost(coords, true);
+ newXform.LocalRotation = 0;
+ }
+ else
+ {
+ Logger.Error("Failed to find a suitable observer spawn point");
+ return;
+ }
+
+ if (spectatorPosition.Eye != null && TryComp(_player.LocalPlayer?.ControlledEntity, out InputMoverComponent? newMover))
+ {
+ newMover.RelativeEntity = spectatorPosition.Eye.Value.Ent;
+ newMover.TargetRelativeRotation = newMover.RelativeRotation = spectatorPosition.Eye.Value.Rot;
+ }
+ }
+
+ private bool TryFindFallbackSpawn(out EntityCoordinates coords)
+ {
+ var uid = EntityQuery()
+ .OrderByDescending(x => x.LocalAABB.Size.LengthSquared)
+ .FirstOrDefault()?.Owner;
+ coords = new EntityCoordinates(uid ?? default, default);
+ return uid != null;
+ }
+
+ private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
+ {
+ if (uid != _player.LocalPlayer?.ControlledEntity)
+ return;
+
+ var xform = Transform(uid);
+ if (xform.MapUid == null || Terminating(xform.MapUid.Value))
+ return;
+
+ SpawnSpectatorGhost(new EntityCoordinates(xform.MapUid.Value, default), true);
+ }
+
+ private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
+ {
+ if (uid != _player.LocalPlayer?.ControlledEntity)
+ return;
+
+ if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
+ return;
+
+ // The entity being spectated from was moved to null-space.
+ // This was probably because they were spectating some entity in a client-side replay that left PVS range.
+ // Simple respawn the ghost.
+ SetSpectatorPosition(default);
+ }
+
+ private void OnDetached(EntityUid uid, ReplaySpectatorComponent component, PlayerDetachedEvent args)
+ {
+ if (uid.IsClientSide())
+ QueueDel(uid);
+ else
+ RemCompDeferred(uid, component);
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs
new file mode 100644
index 0000000000..0d4775a498
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs
@@ -0,0 +1,124 @@
+using Content.Client.Replay.UI;
+using Content.Shared.Verbs;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.Spectator;
+
+// This partial class has methods for spawning a spectator ghost and "possessing" entitites.
+public sealed partial class ReplaySpectatorSystem
+{
+ private void OnGetAlternativeVerbs(GetVerbsEvent ev)
+ {
+ if (_replayPlayback.Replay == null)
+ return;
+
+ var verb = new AlternativeVerb
+ {
+ Priority = 100,
+ Act = () =>
+ {
+ SpectateEntity(ev.Target);
+ },
+
+ Text = Loc.GetString("replay-verb-spectate"),
+ Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/vv.svg.192dpi.png"))
+ };
+
+ ev.Verbs.Add(verb);
+ }
+
+ public void SpectateEntity(EntityUid target)
+ {
+ if (_player.LocalPlayer == null)
+ return;
+
+ var old = _player.LocalPlayer.ControlledEntity;
+
+ if (old == target)
+ {
+ // un-visit
+ SpawnSpectatorGhost(Transform(target).Coordinates, true);
+ return;
+ }
+
+ _player.LocalPlayer.AttachEntity(target, EntityManager, _client);
+ EnsureComp(target);
+
+ _stateMan.RequestStateChange();
+ if (old == null)
+ return;
+
+ if (old.Value.IsClientSide())
+ Del(old.Value);
+ else
+ RemComp(old.Value);
+ }
+
+ public TransformComponent SpawnSpectatorGhost(EntityCoordinates coords, bool gridAttach)
+ {
+ if (_player.LocalPlayer == null)
+ throw new InvalidOperationException();
+
+ var old = _player.LocalPlayer.ControlledEntity;
+
+ var ent = Spawn("MobObserver", coords);
+ _eye.SetMaxZoom(ent, Vector2.One * 5);
+ EnsureComp(ent);
+
+ var xform = Transform(ent);
+
+ if (gridAttach)
+ _transform.AttachToGridOrMap(ent);
+
+ _player.LocalPlayer.AttachEntity(ent, EntityManager, _client);
+
+ if (old != null)
+ {
+ if (old.Value.IsClientSide())
+ QueueDel(old.Value);
+ else
+ RemComp(old.Value);
+ }
+
+ _stateMan.RequestStateChange();
+
+ return xform;
+ }
+
+ private void SpectateCommand(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length == 0)
+ {
+ if (_player.LocalPlayer?.ControlledEntity is { } current)
+ SpawnSpectatorGhost(new EntityCoordinates(current, default), true);
+ else
+ SpawnSpectatorGhost(default, true);
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[0], out var uid))
+ {
+ shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
+ return;
+ }
+
+ if (!Exists(uid))
+ {
+ shell.WriteError(Loc.GetString("cmd-parse-failure-entity-exist", ("arg", args[0])));
+ return;
+ }
+
+ SpectateEntity(uid);
+ }
+
+ private CompletionResult SpectateCompletions(IConsoleShell shell, string[] args)
+ {
+ if (args.Length != 1)
+ return CompletionResult.Empty;
+
+ return CompletionResult.FromHintOptions(CompletionHelper.EntityUids(args[0],
+ EntityManager), Loc.GetString("cmd-replay-spectate-hint"));
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs
new file mode 100644
index 0000000000..add786544e
--- /dev/null
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs
@@ -0,0 +1,77 @@
+using Content.Shared.Movement.Systems;
+using Content.Shared.Verbs;
+using Robust.Client;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Client.Replays.Playback;
+using Robust.Client.State;
+using Robust.Shared.Console;
+
+namespace Content.Client.Replay.Spectator;
+
+///
+/// This system handles spawning replay observer ghosts and maintaining their positions when traveling through time.
+/// It also blocks most normal interactions, just in case.
+///
+///
+/// E.g., if an observer is on a grid, and then jumps forward or backward in time to a point where the grid does not
+/// exist, where should the observer go? This attempts to maintain their position and eye rotation or just re-spawns
+/// them as needed.
+///
+public sealed partial class ReplaySpectatorSystem : EntitySystem
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IConsoleHost _conHost = default!;
+ [Dependency] private readonly IStateManager _stateMan = default!;
+ [Dependency] private readonly TransformSystem _transform = default!;
+ [Dependency] private readonly SharedMoverController _mover = default!;
+ [Dependency] private readonly IBaseClient _client = default!;
+ [Dependency] private readonly SharedContentEyeSystem _eye = default!;
+ [Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
+
+ private SpectatorPosition? _oldPosition;
+ public const string SpectateCmd = "replay_spectate";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetAlternativeVerbs);
+ SubscribeLocalEvent(OnTerminating);
+ SubscribeLocalEvent(OnDetached);
+ SubscribeLocalEvent(OnParentChanged);
+
+ InitializeBlockers();
+
+ _replayPlayback.BeforeSetTick += OnBeforeSetTick;
+ _replayPlayback.AfterSetTick += OnAfterSetTick;
+ _replayPlayback.ReplayPlaybackStarted += OnPlaybackStarted;
+ _replayPlayback.ReplayPlaybackStopped += OnPlaybackStopped;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _replayPlayback.BeforeSetTick -= OnBeforeSetTick;
+ _replayPlayback.AfterSetTick -= OnAfterSetTick;
+ _replayPlayback.ReplayPlaybackStarted -= OnPlaybackStarted;
+ _replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
+ }
+
+ private void OnPlaybackStarted()
+ {
+ InitializeMovement();
+ SetSpectatorPosition(default);
+ _conHost.RegisterCommand(SpectateCmd,
+ Loc.GetString("cmd-replay-spectate-desc"),
+ Loc.GetString("cmd-replay-spectate-help"),
+ SpectateCommand,
+ SpectateCompletions);
+ }
+
+ private void OnPlaybackStopped()
+ {
+ ShutdownMovement();
+ _conHost.UnregisterCommand(SpectateCmd);
+ }
+}
diff --git a/Content.Client/Replay/UI/Loading/LoadingScreen.cs b/Content.Client/Replay/UI/Loading/LoadingScreen.cs
new file mode 100644
index 0000000000..f3f75a2950
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/LoadingScreen.cs
@@ -0,0 +1,51 @@
+using Robust.Client.ResourceManagement;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Shared.CPUJob.JobQueues;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Replay.UI.Loading;
+
+[Virtual]
+public class LoadingScreen : State
+{
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
+
+ public event Action? OnJobFinished;
+ private LoadingScreenControl _screen = default!;
+ public Job? Job;
+
+ public override void FrameUpdate(FrameEventArgs e)
+ {
+ base.FrameUpdate(e);
+ if (Job == null)
+ return;
+
+ Job.Run();
+ if (Job.Status != JobStatus.Finished)
+ return;
+
+ OnJobFinished?.Invoke(Job.Result, Job.Exception);
+ Job = null;
+ }
+
+ protected override void Startup()
+ {
+ _screen = new(_resourceCache);
+ _userInterfaceManager.StateRoot.AddChild(_screen);
+ }
+
+ protected override void Shutdown()
+ {
+ _screen.Dispose();
+ }
+
+ public void UpdateProgress(float value, float maxValue, string header, string subtext = "")
+ {
+ _screen.Bar.Value = value;
+ _screen.Bar.MaxValue = maxValue;
+ _screen.Header.Text = header;
+ _screen.Subtext.Text = subtext;
+ }
+}
diff --git a/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml b/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml
new file mode 100644
index 0000000000..58f353a5ff
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml.cs b/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml.cs
new file mode 100644
index 0000000000..bbb5111438
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/LoadingScreenControl.xaml.cs
@@ -0,0 +1,39 @@
+using Content.Client.Resources;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.UI.Loading;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadingScreenControl : Control
+{
+ public static SpriteSpecifier Sprite = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Silicon/Bots/mommi.rsi"), "wiggle");
+
+ public LoadingScreenControl(IResourceCache resCache)
+ {
+ RobustXamlLoader.Load(this);
+
+ LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
+ Header.FontOverride = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 24);
+ Subtext.FontOverride = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 12);
+
+ SpriteLeft.SetFromSpriteSpecifier(Sprite);
+ SpriteRight.SetFromSpriteSpecifier(Sprite);
+ SpriteLeft.HorizontalAlignment = HAlignment.Stretch;
+ SpriteLeft.VerticalAlignment = VAlignment.Stretch;
+ SpriteLeft.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
+ SpriteRight.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
+
+ Background.PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = Color.FromHex("#303033"),
+ BorderColor = Color.FromHex("#5a5a5a"),
+ BorderThickness = new Thickness(4)
+ };
+ }
+}
diff --git a/Content.Client/Replay/UI/ReplayGhostState.cs b/Content.Client/Replay/UI/ReplayGhostState.cs
new file mode 100644
index 0000000000..284e1198a3
--- /dev/null
+++ b/Content.Client/Replay/UI/ReplayGhostState.cs
@@ -0,0 +1,40 @@
+using Content.Client.UserInterface.Systems.Actions.Widgets;
+using Content.Client.UserInterface.Systems.Alerts.Widgets;
+using Content.Client.UserInterface.Systems.Ghost.Widgets;
+using Content.Client.UserInterface.Systems.Hotbar.Widgets;
+
+namespace Content.Client.Replay.UI;
+
+///
+/// Gameplay state when moving around a replay as a ghost.
+///
+public sealed class ReplayGhostState : ReplaySpectateEntityState
+{
+ protected override void Startup()
+ {
+ base.Startup();
+
+ var screen = UserInterfaceManager.ActiveScreen;
+ if (screen == null)
+ return;
+
+ screen.ShowWidget(false);
+ screen.ShowWidget(false);
+ screen.ShowWidget(false);
+ screen.ShowWidget(false);
+ }
+
+ protected override void Shutdown()
+ {
+ var screen = UserInterfaceManager.ActiveScreen;
+ if (screen != null)
+ {
+ screen.ShowWidget(true);
+ screen.ShowWidget(true);
+ screen.ShowWidget(true);
+ screen.ShowWidget(true);
+ }
+
+ base.Shutdown();
+ }
+}
diff --git a/Content.Client/Replay/UI/ReplaySpectateEntityState.cs b/Content.Client/Replay/UI/ReplaySpectateEntityState.cs
new file mode 100644
index 0000000000..f36c366dae
--- /dev/null
+++ b/Content.Client/Replay/UI/ReplaySpectateEntityState.cs
@@ -0,0 +1,48 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.Chat;
+using Content.Client.UserInterface.Systems.MenuBar.Widgets;
+using Robust.Client.Replays.UI;
+using static Robust.Client.UserInterface.Controls.LayoutContainer;
+
+namespace Content.Client.Replay.UI;
+
+///
+/// Gameplay state when observing/spectating an entity during a replay.
+///
+[Virtual]
+public class ReplaySpectateEntityState : GameplayState
+{
+ protected override void Startup()
+ {
+ base.Startup();
+
+ var screen = UserInterfaceManager.ActiveScreen;
+ if (screen == null)
+ return;
+
+ screen.ShowWidget(false);
+ SetAnchorAndMarginPreset(screen.GetOrAddWidget(), LayoutPreset.TopLeft, margin: 10);
+
+ foreach (var chatbox in UserInterfaceManager.GetUIController().Chats)
+ {
+ chatbox.ChatInput.Visible = false;
+ }
+ }
+
+ protected override void Shutdown()
+ {
+ var screen = UserInterfaceManager.ActiveScreen;
+ if (screen != null)
+ {
+ screen.RemoveWidget();
+ screen.ShowWidget(true);
+ }
+
+ foreach (var chatbox in UserInterfaceManager.GetUIController().Chats)
+ {
+ chatbox.ChatInput.Visible = true;
+ }
+
+ base.Shutdown();
+ }
+}
diff --git a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
index c6d989c0a8..8f83689e80 100644
--- a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
+++ b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
@@ -12,6 +12,7 @@ namespace Content.Client.RoundEnd
public sealed class RoundEndSummaryWindow : DefaultWindow
{
private readonly IEntityManager _entityManager;
+ public int RoundId;
public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, int roundId,
RoundEndMessageEvent.RoundEndPlayerInfo[] info, IEntityManager entityManager)
@@ -28,6 +29,7 @@ public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan,
// "clown slipped the crew x times.", "x shots were fired this round.", etc.
// Also good for serious info.
+ RoundId = roundId;
var roundEndTabs = new TabContainer();
roundEndTabs.AddChild(MakeRoundEndSummaryTab(gm, roundEnd, roundTimeSpan, roundId));
roundEndTabs.AddChild(MakePlayerManifestoTab(info));
diff --git a/Content.Client/UserInterface/Controls/FancyTree/FancyTree.xaml.cs b/Content.Client/UserInterface/Controls/FancyTree/FancyTree.xaml.cs
index 1df5864926..658465f245 100644
--- a/Content.Client/UserInterface/Controls/FancyTree/FancyTree.xaml.cs
+++ b/Content.Client/UserInterface/Controls/FancyTree/FancyTree.xaml.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Content.Client.Resources;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -196,13 +197,38 @@ public void RecursiveSetExpanded(TreeItem item, bool value, int depth)
if (depth == 0)
return;
depth--;
-
+
foreach (var child in item.Body.Children)
{
RecursiveSetExpanded((TreeItem) child, value, depth);
}
}
+ public bool TryGetIndexFromMetadata(object metadata, [NotNullWhen(true)] out int? index)
+ {
+ index = null;
+ foreach (var item in Items)
+ {
+ if (item.Metadata?.Equals(metadata) ?? false)
+ {
+ index = item.Index;
+ break;
+ }
+ }
+ return index != null;
+ }
+
+ public void ExpandParentEntries(int index)
+ {
+ Control? current = Items[index];
+ while (current != null)
+ {
+ if (current is TreeItem item)
+ item.SetExpanded(true);
+ current = current.Parent;
+ }
+ }
+
public void Clear()
{
foreach (var item in Items)
diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
index 472ad49a3f..d325009912 100644
--- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
+++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
@@ -108,18 +108,6 @@ private void OnScreenUnload()
public void OnStateEntered(GameplayState state)
{
- DebugTools.Assert(_window == null);
-
- _window = UIManager.CreateWindow();
- LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
-
- _window.OnOpen += OnWindowOpened;
- _window.OnClose += OnWindowClosed;
- _window.ClearButton.OnPressed += OnClearPressed;
- _window.SearchBar.OnTextChanged += OnSearchChanged;
- _window.FilterButton.OnItemSelected += OnFilterSelected;
-
-
if (_actionsSystem != null)
{
_actionsSystem.ActionAdded += OnActionAdded;
@@ -328,18 +316,6 @@ public void OnStateExited(GameplayState state)
_actionsSystem.ActionsUpdated -= OnActionsUpdated;
}
- if (_window != null)
- {
- _window.OnOpen -= OnWindowOpened;
- _window.OnClose -= OnWindowClosed;
- _window.ClearButton.OnPressed -= OnClearPressed;
- _window.SearchBar.OnTextChanged -= OnSearchChanged;
- _window.FilterButton.OnItemSelected -= OnFilterSelected;
-
- _window.Dispose();
- _window = null;
- }
-
CommandBinds.Unregister();
}
@@ -779,10 +755,32 @@ private void UnloadGui()
ActionsBar.PageButtons.LeftArrow.OnPressed -= OnLeftArrowPressed;
ActionsBar.PageButtons.RightArrow.OnPressed -= OnRightArrowPressed;
+
+ if (_window != null)
+ {
+ _window.OnOpen -= OnWindowOpened;
+ _window.OnClose -= OnWindowClosed;
+ _window.ClearButton.OnPressed -= OnClearPressed;
+ _window.SearchBar.OnTextChanged -= OnSearchChanged;
+ _window.FilterButton.OnItemSelected -= OnFilterSelected;
+
+ _window.Dispose();
+ _window = null;
+ }
}
private void LoadGui()
{
+ DebugTools.Assert(_window == null);
+ _window = UIManager.CreateWindow();
+ LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
+
+ _window.OnOpen += OnWindowOpened;
+ _window.OnClose += OnWindowClosed;
+ _window.ClearButton.OnPressed += OnClearPressed;
+ _window.SearchBar.OnTextChanged += OnSearchChanged;
+ _window.FilterButton.OnItemSelected += OnFilterSelected;
+
if (ActionsBar == null)
{
return;
@@ -791,7 +789,6 @@ private void LoadGui()
ActionsBar.PageButtons.LeftArrow.OnPressed += OnLeftArrowPressed;
ActionsBar.PageButtons.RightArrow.OnPressed += OnRightArrowPressed;
-
RegisterActionContainer(ActionsBar.ActionsContainer);
_actionsSystem?.LinkAllActions();
diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs
index c55c3df841..1fcd7fd184 100644
--- a/Content.Client/Verbs/VerbSystem.cs
+++ b/Content.Client/Verbs/VerbSystem.cs
@@ -214,7 +214,7 @@ public void ExecuteVerb(EntityUid target, Verb verb)
return;
}
- if (verb.ClientExclusive)
+ if (verb.ClientExclusive || target.IsClientSide())
// is this a client exclusive (gui) verb?
ExecuteVerb(verb, user.Value, target);
else
diff --git a/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs b/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs
index 72439de236..9f5193a3ca 100644
--- a/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs
+++ b/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs
@@ -104,7 +104,7 @@ protected override void Draw(in OverlayDrawArgs args)
const float endX = 22f;
// Area marking where to release
- var releaseWidth = 2f * SharedMeleeWeaponSystem.GracePeriod / (float) comp.WindupTime.TotalSeconds * EyeManager.PixelsPerMeter;
+ var releaseWidth = 2f * SharedMeleeWeaponSystem.GracePeriod / (float) _melee.GetWindupTime(meleeUid, owner.Value, comp).TotalSeconds * EyeManager.PixelsPerMeter;
const float releaseMiddle = (endX - startX) / 2f + startX;
var releaseBox = new Box2(new Vector2(releaseMiddle - releaseWidth / 2f, 3f) / EyeManager.PixelsPerMeter,
@@ -114,7 +114,7 @@ protected override void Draw(in OverlayDrawArgs args)
handle.DrawRect(releaseBox, Color.LimeGreen);
// Wraps around back to 0
- var totalDuration = comp.WindupTime.TotalSeconds * 2;
+ var totalDuration = _melee.GetWindupTime(meleeUid, owner.Value, comp).TotalSeconds * 2;
var elapsed = (currentTime - comp.WindUpStart.Value).TotalSeconds % (2 * totalDuration);
var value = elapsed / totalDuration;
diff --git a/Content.IntegrationTests/Tests/Body/LegTest.cs b/Content.IntegrationTests/Tests/Body/LegTest.cs
index d959db5da1..3930e3dfb3 100644
--- a/Content.IntegrationTests/Tests/Body/LegTest.cs
+++ b/Content.IntegrationTests/Tests/Body/LegTest.cs
@@ -35,14 +35,13 @@ public async Task RemoveLegsFallTest()
var server = pairTracker.Pair.Server;
AppearanceComponent appearance = null;
+ var entityManager = server.ResolveDependency();
+ var mapManager = server.ResolveDependency();
await server.WaitAssertion(() =>
{
- var mapManager = IoCManager.Resolve();
-
var mapId = mapManager.CreateMap();
- var entityManager = IoCManager.Resolve();
var human = entityManager.SpawnEntity("HumanBodyAndAppearanceDummy",
new MapCoordinates(Vector2.Zero, mapId));
@@ -52,7 +51,7 @@ await server.WaitAssertion(() =>
Assert.That(!appearance.TryGetData(RotationVisuals.RotationState, out RotationState _));
var bodySystem = entityManager.System();
- var legs = bodySystem.GetBodyChildrenOfType(body.Owner, BodyPartType.Leg, body);
+ var legs = bodySystem.GetBodyChildrenOfType(human, BodyPartType.Leg, body);
foreach (var leg in legs)
{
diff --git a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
index 43d9d3889d..78619f98f4 100644
--- a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
+++ b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs
@@ -20,11 +20,11 @@ public async Task EuiManagerRecycleWithOpenWindowTest()
var server = pairTracker.Pair.Server;
var sPlayerManager = server.ResolveDependency();
+ var eui = server.ResolveDependency();
await server.WaitAssertion(() =>
{
var clientSession = sPlayerManager.ServerSessions.Single();
- var eui = IoCManager.Resolve();
var ui = new AdminAnnounceEui();
eui.OpenEui(ui, clientSession);
});
diff --git a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
index 4f1e72ed2f..0ea036d8e1 100644
--- a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
+++ b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
@@ -42,14 +42,15 @@ public async Task TestA()
var c = pairTracker.Pair.Client;
var cEntities = c.ResolveDependency();
+ var ent = s.ResolveDependency();
EntityUid dummy = default;
- var ent2 = s.ResolveDependency();
+ var mapManager = s.ResolveDependency();
+ var mapId = mapManager.CreateMap();
+
await s.WaitPost(() =>
{
- var mapId = ent2.GetAllMapIds().Last();
var pos = new MapCoordinates(Vector2.Zero, mapId);
- var ent = IoCManager.Resolve();
var entStorage = ent.EntitySysManager.GetEntitySystem();
var container = ent.SpawnEntity("ContainerOcclusionA", pos);
dummy = ent.SpawnEntity("ContainerOcclusionDummy", pos);
@@ -78,14 +79,15 @@ public async Task TestB()
var c = pairTracker.Pair.Client;
var cEntities = c.ResolveDependency();
- var ent2 = s.ResolveDependency();
+ var ent = s.ResolveDependency();
EntityUid dummy = default;
+ var mapManager = s.ResolveDependency();
+ var mapId = mapManager.CreateMap();
+
await s.WaitPost(() =>
{
- var mapId = ent2.GetAllMapIds().Last();
var pos = new MapCoordinates(Vector2.Zero, mapId);
- var ent = IoCManager.Resolve();
var entStorage = ent.EntitySysManager.GetEntitySystem();
var container = ent.SpawnEntity("ContainerOcclusionB", pos);
dummy = ent.SpawnEntity("ContainerOcclusionDummy", pos);
@@ -113,15 +115,16 @@ public async Task TestAb()
var s = pairTracker.Pair.Server;
var c = pairTracker.Pair.Client;
- var ent2 = s.ResolveDependency();
var cEntities = c.ResolveDependency();
+ var ent = s.ResolveDependency();
EntityUid dummy = default;
+ var mapManager = s.ResolveDependency();
+ var mapId = mapManager.CreateMap();
+
await s.WaitPost(() =>
{
- var mapId = ent2.GetAllMapIds().Last();
var pos = new MapCoordinates(Vector2.Zero, mapId);
- var ent = IoCManager.Resolve();
var entStorage = ent.EntitySysManager.GetEntitySystem();
var containerA = ent.SpawnEntity("ContainerOcclusionA", pos);
var containerB = ent.SpawnEntity("ContainerOcclusionB", pos);
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index 35d8afe458..9920e15e02 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -105,7 +105,7 @@ await server.WaitPost(() =>
var coordinates = new MapCoordinates(0, 0, map);
sDamageableEntity = sEntityManager.SpawnEntity("TestDamageableEntityId", coordinates);
- sDamageableComponent = IoCManager.Resolve().GetComponent(sDamageableEntity);
+ sDamageableComponent = sEntityManager.GetComponent(sDamageableEntity);
sDamageableSystem = sEntitySystemManager.GetEntitySystem();
group1 = sPrototypeManager.Index("TestGroup1");
diff --git a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
index 6b43d904c8..d1d557964b 100644
--- a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
+++ b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs
@@ -20,14 +20,15 @@ public async Task Test()
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
var server = pairTracker.Pair.Server;
var testMap = await PoolManager.CreateTestMap(pairTracker);
+ var entMgr = server.ResolveDependency();
+ var sysManager = server.ResolveDependency();
var coordinates = testMap.GridCoords;
await server.WaitAssertion(() =>
{
// Spawn everything.
- var invSystem = IoCManager.Resolve().GetEntitySystem();
+ var invSystem = sysManager.GetEntitySystem();
- var entMgr = IoCManager.Resolve();
var container = entMgr.SpawnEntity(null, coordinates);
entMgr.EnsureComponent(container);
entMgr.EnsureComponent(container);
@@ -35,7 +36,7 @@ await server.WaitAssertion(() =>
var child = entMgr.SpawnEntity(null, coordinates);
var item = entMgr.EnsureComponent(child);
- IoCManager.Resolve().GetEntitySystem().SetSlots(item.Owner, SlotFlags.HEAD, item);
+ sysManager.GetEntitySystem().SetSlots(child, SlotFlags.HEAD, item);
// Equip item.
Assert.That(invSystem.TryEquip(container, child, "head"), Is.True);
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
index 29c25be5d9..3cc917add9 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
@@ -26,6 +26,7 @@ public async Task Test()
var sEntityManager = server.ResolveDependency();
var sEntitySystemManager = server.ResolveDependency();
+ var protoManager = server.ResolveDependency();
EntityUid sDestructibleEntity = default;
DamageableComponent sDamageableComponent = null;
@@ -37,7 +38,7 @@ await server.WaitPost(() =>
var coordinates = testMap.GridCoords;
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageTypeEntityId, coordinates);
- sDamageableComponent = IoCManager.Resolve().GetComponent(sDestructibleEntity);
+ sDamageableComponent = sEntityManager.GetComponent(sDestructibleEntity);
sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
sDamageableSystem = sEntitySystemManager.GetEntitySystem();
@@ -52,8 +53,8 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- var bluntDamageType = IoCManager.Resolve().Index("TestBlunt");
- var slashDamageType = IoCManager.Resolve().Index("TestSlash");
+ var bluntDamageType = protoManager.Index("TestBlunt");
+ var slashDamageType = protoManager.Index("TestSlash");
var bluntDamage = new DamageSpecifier(bluntDamageType,5);
var slashDamage = new DamageSpecifier(slashDamageType,5);
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
index be26c38c59..c8a38734e7 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
@@ -41,7 +41,7 @@ await server.WaitPost(() =>
await server.WaitAssertion(() =>
{
- var coordinates = IoCManager.Resolve().GetComponent(sDestructibleEntity).Coordinates;
+ var coordinates = sEntityManager.GetComponent(sDestructibleEntity).Coordinates;
var bruteDamageGroup = sPrototypeManager.Index("TestBrute");
DamageSpecifier bruteDamage = new(bruteDamageGroup,50);
diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
index c383335241..4d8616bd81 100644
--- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
+++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
@@ -156,6 +156,8 @@ public async Task Test()
EntityUid wrench = default!;
EntityUid disposalUnit = default!;
EntityUid disposalTrunk = default!;
+
+ EntityUid unitUid = default;
DisposalUnitComponent unitComponent = default!;
var entityManager = server.ResolveDependency();
@@ -168,22 +170,22 @@ await server.WaitAssertion(() =>
wrench = entityManager.SpawnEntity("WrenchDummy", coordinates);
disposalUnit = entityManager.SpawnEntity("DisposalUnitDummy", coordinates);
disposalTrunk = entityManager.SpawnEntity("DisposalTrunkDummy",
- IoCManager.Resolve().GetComponent(disposalUnit).MapPosition);
+ entityManager.GetComponent(disposalUnit).MapPosition);
// Test for components existing
- ref DisposalUnitComponent? comp = ref unitComponent!;
- Assert.True(entityManager.TryGetComponent(disposalUnit, out comp));
+ unitUid = disposalUnit;
+ Assert.True(entityManager.TryGetComponent(disposalUnit, out unitComponent));
Assert.True(entityManager.HasComponent(disposalTrunk));
// Can't insert, unanchored and unpowered
- entityManager.GetComponent(unitComponent!.Owner).Anchored = false;
+ entityManager.GetComponent(unitUid).Anchored = false;
UnitInsertContains(unitComponent, false, human, wrench, disposalUnit, disposalTrunk);
});
await server.WaitAssertion(() =>
{
// Anchor the disposal unit
- entityManager.GetComponent(unitComponent.Owner).Anchored = true;
+ entityManager.GetComponent(unitUid).Anchored = true;
// No power
Assert.False(unitComponent.Powered);
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
index db2df0ade2..390bdff8d3 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
@@ -67,13 +67,14 @@ public async Task TestFinished()
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency();
+ var timing = server.ResolveDependency();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem();
var ev = new TestDoAfterEvent();
// That it finishes successfully
await server.WaitPost(() =>
{
- var tickTime = 1.0f / IoCManager.Resolve().TickRate;
+ var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(mob, tickTime / 2, ev, null) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args));
@@ -92,14 +93,14 @@ public async Task TestCancelled()
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
var entityManager = server.ResolveDependency();
+ var timing = server.ResolveDependency();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem();
- DoAfterId? id = default;
+ DoAfterId? id;
var ev = new TestDoAfterEvent();
-
await server.WaitPost(() =>
{
- var tickTime = 1.0f / IoCManager.Resolve().TickRate;
+ var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(mob, tickTime * 2, ev, null) { Broadcast = true };
diff --git a/Content.IntegrationTests/Tests/DummyIconTest.cs b/Content.IntegrationTests/Tests/DummyIconTest.cs
index f8351848cf..d51da122d4 100644
--- a/Content.IntegrationTests/Tests/DummyIconTest.cs
+++ b/Content.IntegrationTests/Tests/DummyIconTest.cs
@@ -17,14 +17,15 @@ public async Task Test()
{
await using var pairTracker = await PoolManager.GetServerClient();
var client = pairTracker.Pair.Client;
+ var prototypeManager = client.ResolveDependency();
+ var resourceCache = client.ResolveDependency();
await client.WaitAssertion(() =>
{
- var prototypeManager = IoCManager.Resolve();
- var resourceCache = IoCManager.Resolve();
foreach (var proto in prototypeManager.EnumeratePrototypes())
{
- if (proto.NoSpawn || proto.Abstract || !proto.Components.ContainsKey("Sprite")) continue;
+ if (proto.NoSpawn || proto.Abstract || !proto.Components.ContainsKey("Sprite"))
+ continue;
Assert.DoesNotThrow(() =>
{
diff --git a/Content.IntegrationTests/Tests/FollowerSystemTest.cs b/Content.IntegrationTests/Tests/FollowerSystemTest.cs
index 823c94b488..ceef0f35b6 100644
--- a/Content.IntegrationTests/Tests/FollowerSystemTest.cs
+++ b/Content.IntegrationTests/Tests/FollowerSystemTest.cs
@@ -22,12 +22,14 @@ public async Task FollowerMapDeleteTest()
await using var pairTracker = await PoolManager.GetServerClient(new (){NoClient = true});
var server = pairTracker.Pair.Server;
+ var entMan = server.ResolveDependency();
+ var mapMan = server.ResolveDependency();
+ var sysMan = server.ResolveDependency();
+ var logMan = server.ResolveDependency();
+ var logger = logMan.RootSawmill;
+
await server.WaitPost(() =>
{
- var mapMan = IoCManager.Resolve();
- var entMan = IoCManager.Resolve();
- var sysMan = IoCManager.Resolve();
- var logger = IoCManager.Resolve().RootSawmill;
var followerSystem = sysMan.GetEntitySystem();
// Create a map to spawn the observers on.
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
index df660c7761..478972d3ea 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
@@ -49,13 +49,15 @@ public async Task Test()
CuffableComponent cuffed;
HandsComponent hands;
+ var entityManager = server.ResolveDependency();
+ var mapManager = server.ResolveDependency();
+ var host = server.ResolveDependency();
+
await server.WaitAssertion(() =>
{
- var mapManager = IoCManager.Resolve();
var mapId = mapManager.CreateMap();
var coordinates = new MapCoordinates(Vector2.Zero, mapId);
- var entityManager = IoCManager.Resolve();
var cuffableSys = entityManager.System();
// Spawn the entities
@@ -81,8 +83,8 @@ await server.WaitAssertion(() =>
"Handcuffing a player did not result in their hands being cuffed");
// Test to ensure a player with 4 hands will still only have 2 hands cuffed
- AddHand(human);
- AddHand(human);
+ AddHand(human, host);
+ AddHand(human, host);
Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2));
Assert.That(hands.SortedHands.Count, Is.EqualTo(4));
@@ -95,9 +97,8 @@ await server.WaitAssertion(() =>
await pairTracker.CleanReturnAsync();
}
- private void AddHand(EntityUid to)
+ private void AddHand(EntityUid to, IServerConsoleHost host)
{
- var host = IoCManager.Resolve();
host.ExecuteCommand(null, $"addhand {to}");
}
}
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index d4634af6f8..b1ea932b9e 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -38,6 +38,7 @@ public async Task Test()
EntityUid generator = default;
var entityMan = server.ResolveDependency();
+ var mapMan = server.ResolveDependency();
MapGridComponent grid1 = null;
MapGridComponent grid2 = null;
@@ -45,8 +46,6 @@ public async Task Test()
// Create grids
await server.WaitAssertion(() =>
{
- var mapMan = IoCManager.Resolve();
-
var mapId = testMap.MapId;
grid1 = mapMan.CreateGrid(mapId);
grid2 = mapMan.CreateGrid(mapId);
diff --git a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
index 65e68d7b9a..9aa19d8d4f 100644
--- a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
+++ b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
@@ -69,12 +69,11 @@ public async Task Test()
EntityUid pocketItem = default;
InventorySystem invSystem = default!;
+ var entityMan = server.ResolveDependency();
await server.WaitAssertion(() =>
{
- invSystem = IoCManager.Resolve().GetEntitySystem();
- var mapMan = IoCManager.Resolve();
- var entityMan = IoCManager.Resolve();
+ invSystem = entityMan.System();
human = entityMan.SpawnEntity("HumanDummy", coordinates);
uniform = entityMan.SpawnEntity("UniformDummy", coordinates);
@@ -96,8 +95,8 @@ await server.WaitAssertion(() =>
Assert.That(invSystem.CanEquip(human, tooBigItem, "pocket1", out _), Is.False); // Still failing!
Assert.That(invSystem.TryEquip(human, pocketItem, "pocket1"));
- Assert.That(IsDescendant(idCard, human));
- Assert.That(IsDescendant(pocketItem, human));
+ Assert.That(IsDescendant(idCard, human, entityMan));
+ Assert.That(IsDescendant(pocketItem, human, entityMan));
// Now drop the jumpsuit.
Assert.That(invSystem.TryUnequip(human, "jumpsuit"));
@@ -108,9 +107,9 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
// Items have been dropped!
- Assert.That(IsDescendant(uniform, human), Is.False);
- Assert.That(IsDescendant(idCard, human), Is.False);
- Assert.That(IsDescendant(pocketItem, human), Is.False);
+ Assert.That(IsDescendant(uniform, human, entityMan), Is.False);
+ Assert.That(IsDescendant(idCard, human, entityMan), Is.False);
+ Assert.That(IsDescendant(pocketItem, human, entityMan), Is.False);
// Ensure everything null here.
Assert.That(!invSystem.TryGetSlotEntity(human, "jumpsuit", out _));
@@ -121,9 +120,9 @@ await server.WaitAssertion(() =>
await pairTracker.CleanReturnAsync();
}
- private static bool IsDescendant(EntityUid descendant, EntityUid parent)
+ private static bool IsDescendant(EntityUid descendant, EntityUid parent, IEntityManager entManager)
{
- var xforms = IoCManager.Resolve().GetEntityQuery();
+ var xforms = entManager.GetEntityQuery();
var tmpParent = xforms.GetComponent(descendant).ParentUid;
while (tmpParent.IsValid())
{
diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
index 6a7d8c8c13..d1fafa0d73 100644
--- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
+++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs
@@ -47,11 +47,10 @@ public async Task SpawnItemInSlotTest()
var server = pairTracker.Pair.Server;
var sEntities = server.ResolveDependency();
+ var systemMan = sEntities.EntitySysManager;
await server.WaitAssertion(() =>
{
- var mapMan = IoCManager.Resolve();
- var systemMan = IoCManager.Resolve();
var human = sEntities.SpawnEntity("InventoryStunnableDummy", MapCoordinates.Nullspace);
var invSystem = systemMan.GetEntitySystem();
diff --git a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
index f9e6df00b3..2e2cfa4f58 100644
--- a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
+++ b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs
@@ -13,18 +13,18 @@ public sealed class ServerReloginTest
[Test]
public async Task Relogin()
{
- IConfigurationManager serverConfig = default;
- IPlayerManager serverPlayerMgr = default;
- IClientNetManager clientNetManager = default;
await using var pairTracker = await PoolManager.GetServerClient();
var server = pairTracker.Pair.Server;
var client = pairTracker.Pair.Client;
int originalMaxPlayers = 0;
string username = null;
+
+ var serverConfig = server.ResolveDependency();
+ var serverPlayerMgr = server.ResolveDependency();
+ var clientNetManager = client.ResolveDependency();
+
await server.WaitAssertion(() =>
{
- serverConfig = IoCManager.Resolve();
- serverPlayerMgr = IoCManager.Resolve();
Assert.That(serverPlayerMgr.PlayerCount, Is.EqualTo(1));
originalMaxPlayers = serverConfig.GetCVar(CCVars.SoftMaxPlayers);
username = serverPlayerMgr.Sessions.First().Name;
@@ -35,7 +35,6 @@ await server.WaitAssertion(() =>
await client.WaitAssertion(() =>
{
- clientNetManager = IoCManager.Resolve();
clientNetManager.ClientDisconnect("For testing");
});
diff --git a/Content.IntegrationTests/Tests/MachineBoardTest.cs b/Content.IntegrationTests/Tests/MachineBoardTest.cs
index e1c1718cfb..5ffe0cf91c 100644
--- a/Content.IntegrationTests/Tests/MachineBoardTest.cs
+++ b/Content.IntegrationTests/Tests/MachineBoardTest.cs
@@ -20,9 +20,9 @@ public sealed class MachineBoardTest
"MachineParticleAcceleratorFuelChamberCircuitboard",
"MachineParticleAcceleratorFuelChamberCircuitboard",
"MachineParticleAcceleratorPowerBoxCircuitboard",
- "MachineParticleAcceleratorEmitterLeftCircuitboard",
- "MachineParticleAcceleratorEmitterCenterCircuitboard",
- "MachineParticleAcceleratorEmitterRightCircuitboard",
+ "MachineParticleAcceleratorEmitterStarboardCircuitboard",
+ "MachineParticleAcceleratorEmitterForeCircuitboard",
+ "MachineParticleAcceleratorEmitterPortCircuitboard",
"ParticleAcceleratorComputerCircuitboard"
};
@@ -33,7 +33,7 @@ public sealed class MachineBoardTest
[Test]
public async Task TestMachineBoardHasValidMachine()
{
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
var server = pairTracker.Pair.Server;
var protoMan = server.ResolveDependency();
@@ -46,13 +46,16 @@ await server.WaitAssertion(() =>
continue;
var mId = mbc.Prototype;
- Assert.That(mId, Is.Not.Null, $"Machine board {p.ID} does not have a corresponding machine.");
- Assert.That(protoMan.TryIndex(mId, out var mProto),
- $"Machine board {p.ID}'s corresponding machine has an invalid prototype.");
- Assert.That(mProto.TryGetComponent(out var mComp),
- $"Machine board {p.ID}'s corresponding machine {mId} does not have MachineComponent");
- Assert.That(mComp.BoardPrototype, Is.EqualTo(p.ID),
- $"Machine {mId}'s BoardPrototype is not equal to it's corresponding machine board, {p.ID}");
+ Assert.Multiple(() =>
+ {
+ Assert.That(mId, Is.Not.Null, $"Machine board {p.ID} does not have a corresponding machine.");
+ Assert.That(protoMan.TryIndex(mId, out var mProto),
+ $"Machine board {p.ID}'s corresponding machine has an invalid prototype.");
+ Assert.That(mProto.TryGetComponent(out var mComp),
+ $"Machine board {p.ID}'s corresponding machine {mId} does not have MachineComponent");
+ Assert.That(mComp.BoardPrototype, Is.EqualTo(p.ID),
+ $"Machine {mId}'s BoardPrototype is not equal to it's corresponding machine board, {p.ID}");
+ });
}
});
@@ -66,7 +69,7 @@ await server.WaitAssertion(() =>
[Test]
public async Task TestComputerBoardHasValidComputer()
{
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
+ await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
var server = pairTracker.Pair.Server;
var protoMan = server.ResolveDependency();
@@ -79,13 +82,16 @@ await server.WaitAssertion(() =>
continue;
var cId = cbc.Prototype;
- Assert.That(cId, Is.Not.Null, $"Computer board \"{p.ID}\" does not have a corresponding computer.");
- Assert.That(protoMan.TryIndex(cId, out var cProto),
- $"Computer board \"{p.ID}\"'s corresponding computer has an invalid prototype.");
- Assert.That(cProto.TryGetComponent(out var cComp),
- $"Computer board {p.ID}'s corresponding computer \"{cId}\" does not have ComputerComponent");
- Assert.That(cComp.BoardPrototype, Is.EqualTo(p.ID),
- $"Computer \"{cId}\"'s BoardPrototype is not equal to it's corresponding computer board, \"{p.ID}\"");
+ Assert.Multiple(() =>
+ {
+ Assert.That(cId, Is.Not.Null, $"Computer board \"{p.ID}\" does not have a corresponding computer.");
+ Assert.That(protoMan.TryIndex(cId, out var cProto),
+ $"Computer board \"{p.ID}\"'s corresponding computer has an invalid prototype.");
+ Assert.That(cProto.TryGetComponent(out var cComp),
+ $"Computer board {p.ID}'s corresponding computer \"{cId}\" does not have ComputerComponent");
+ Assert.That(cComp.BoardPrototype, Is.EqualTo(p.ID),
+ $"Computer \"{cId}\"'s BoardPrototype is not equal to it's corresponding computer board, \"{p.ID}\"");
+ });
}
});
diff --git a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs
index 5f97339afd..893694d509 100644
--- a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs
+++ b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs
@@ -16,7 +16,10 @@ public async Task Test()
var server = pairTracker.Pair.Server;
var client = pairTracker.Pair.Client;
- await client.WaitPost(() => IoCManager.Resolve().ExecuteCommand("disconnect"));
+ var host = client.ResolveDependency();
+ var netManager = client.ResolveDependency();
+
+ await client.WaitPost(() => host.ExecuteCommand("disconnect"));
// Run some ticks for the disconnect to complete and such.
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
@@ -26,7 +29,7 @@ public async Task Test()
// Reconnect.
client.SetConnectTarget(server);
- await client.WaitPost(() => IoCManager.Resolve().ClientConnect(null, 0, null));
+ await client.WaitPost(() => netManager.ClientConnect(null, 0, null));
// Run some ticks for the handshake to complete and such.
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
index 869be4c4fe..b7000b5d20 100644
--- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
+++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
@@ -70,7 +70,7 @@ await server.WaitPost(() =>
var map = sMapManager.CreateMap();
var player = sPlayerManager.ServerSessions.Single();
serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates((0, 0), map));
- serverComponent = IoCManager.Resolve().AddComponent(serverEnt);
+ serverComponent = sEntityManager.AddComponent(serverEnt);
// Make client "join game" so they receive game state updates.
player.JoinGame();
@@ -364,7 +364,7 @@ await client.WaitPost(() =>
Assert.That(clientComponent.Foo, Is.True);
}
}
-
+
cfg.SetCVar(CVars.NetLogging, log);
await pairTracker.CleanReturnAsync();
}
@@ -432,7 +432,7 @@ public override void Initialize()
private void HandleMessage(SetFooMessage message, EntitySessionEventArgs args)
{
- var component = IoCManager.Resolve().GetComponent(message.Uid);
+ var component = EntityManager.GetComponent(message.Uid);
var old = component.Foo;
if (Allow)
{
diff --git a/Content.IntegrationTests/Tests/RestartRoundTest.cs b/Content.IntegrationTests/Tests/RestartRoundTest.cs
index 6798700224..f54bd74e22 100644
--- a/Content.IntegrationTests/Tests/RestartRoundTest.cs
+++ b/Content.IntegrationTests/Tests/RestartRoundTest.cs
@@ -14,10 +14,11 @@ public async Task Test()
{
await using var pairTracker = await PoolManager.GetServerClient();
var server = pairTracker.Pair.Server;
+ var sysManager = server.ResolveDependency();
await server.WaitPost(() =>
{
- IoCManager.Resolve().GetEntitySystem().RestartRound();
+ sysManager.GetEntitySystem().RestartRound();
});
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs
index 9e4b71d8c2..2b8143038a 100644
--- a/Content.IntegrationTests/Tests/RoundEndTest.cs
+++ b/Content.IntegrationTests/Tests/RoundEndTest.cs
@@ -22,6 +22,7 @@ public async Task Test()
var server = pairTracker.Pair.Server;
+ var entManager = server.ResolveDependency();
var config = server.ResolveDependency();
var sysManager = server.ResolveDependency();
var ticker = sysManager.GetEntitySystem();
@@ -42,7 +43,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- var bus = IoCManager.Resolve().EventBus;
+ var bus = entManager.EventBus;
bus.SubscribeEvent(EventSource.Local, this, _ => {
Interlocked.Increment(ref eventCount);
});
diff --git a/Content.Replay/Content.Replay.csproj b/Content.Replay/Content.Replay.csproj
new file mode 100644
index 0000000000..5d5880f23a
--- /dev/null
+++ b/Content.Replay/Content.Replay.csproj
@@ -0,0 +1,26 @@
+
+
+ $(TargetFramework)
+ 10
+ false
+ false
+ ..\bin\Content.Replay\
+ Exe
+ nullable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Replay/EntryPoint.cs b/Content.Replay/EntryPoint.cs
new file mode 100644
index 0000000000..ed6460a7e7
--- /dev/null
+++ b/Content.Replay/EntryPoint.cs
@@ -0,0 +1,34 @@
+using Content.Client.Replay;
+using Content.Replay.Menu;
+using JetBrains.Annotations;
+using Robust.Client;
+using Robust.Client.Console;
+using Robust.Client.State;
+using Robust.Shared.ContentPack;
+
+namespace Content.Replay;
+
+[UsedImplicitly]
+public sealed class EntryPoint : GameClient
+{
+ [Dependency] private readonly IBaseClient _client = default!;
+ [Dependency] private readonly IStateManager _stateMan = default!;
+ [Dependency] private readonly ContentReplayPlaybackManager _contentReplayPlaybackMan = default!;
+ [Dependency] private readonly IClientConGroupController _conGrp = default!;
+
+ public override void Init()
+ {
+ base.Init();
+ IoCManager.BuildGraph();
+ IoCManager.InjectDependencies(this);
+ }
+
+ public override void PostInit()
+ {
+ base.PostInit();
+ _client.StartSinglePlayer();
+ _conGrp.Implementation = new ReplayConGroup();
+ _contentReplayPlaybackMan.DefaultState = typeof(ReplayMainScreen);
+ _stateMan.RequestStateChange();
+ }
+}
diff --git a/Content.Replay/GlobalUsings.cs b/Content.Replay/GlobalUsings.cs
new file mode 100644
index 0000000000..f0cbfd5b1a
--- /dev/null
+++ b/Content.Replay/GlobalUsings.cs
@@ -0,0 +1,9 @@
+// Global usings for Content.Replay
+
+global using System;
+global using System.Collections.Generic;
+global using Robust.Shared.Log;
+global using Robust.Shared.Localization;
+global using Robust.Shared.GameObjects;
+global using Robust.Shared.IoC;
+global using Robust.Shared.Maths;
diff --git a/Content.Replay/Menu/ReplayMainMenu.cs b/Content.Replay/Menu/ReplayMainMenu.cs
new file mode 100644
index 0000000000..6748c20988
--- /dev/null
+++ b/Content.Replay/Menu/ReplayMainMenu.cs
@@ -0,0 +1,283 @@
+using System.Linq;
+using Content.Client.Message;
+using Content.Client.UserInterface.Systems.EscapeMenu;
+using Robust.Client;
+using Robust.Client.Replays.Loading;
+using Robust.Client.ResourceManagement;
+using Robust.Client.Serialization;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Utility;
+using TerraFX.Interop.Windows;
+using static Robust.Shared.Replays.IReplayRecordingManager;
+using IResourceManager = Robust.Shared.ContentPack.IResourceManager;
+
+namespace Content.Replay.Menu;
+
+///
+/// Main menu screen for selecting and loading replays.
+///
+public sealed class ReplayMainScreen : State
+{
+ [Dependency] private readonly IResourceManager _resMan = default!;
+ [Dependency] private readonly IComponentFactory _factory = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IReplayLoadManager _loadMan = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IGameController _controllerProxy = default!;
+ [Dependency] private readonly IClientRobustSerializer _serializer = default!;
+ [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
+
+ private ReplayMainMenuControl _mainMenuControl = default!;
+ private SelectReplayWindow? _selectWindow;
+ private ResPath _directory;
+ private List<(string Name, ResPath Path)> _replays = new();
+ private ResPath? _selected;
+
+ protected override void Startup()
+ {
+ _mainMenuControl = new(_resourceCache);
+ _userInterfaceManager.StateRoot.AddChild(_mainMenuControl);
+
+ _mainMenuControl.SelectButton.OnPressed += OnSelectPressed;
+ _mainMenuControl.QuitButton.OnPressed += QuitButtonPressed;
+ _mainMenuControl.OptionsButton.OnPressed += OptionsButtonPressed;
+ _mainMenuControl.FolderButton.OnPressed += OnFolderPressed;
+ _mainMenuControl.LoadButton.OnPressed += OnLoadpressed;
+
+ _directory = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath();
+ RefreshReplays();
+ SelectReplay(_replays.FirstOrNull()?.Path);
+ if (_selected == null) // force initial update
+ UpdateSelectedInfo();
+ }
+
+ ///
+ /// Read replay meta-data and update the replay info box.
+ ///
+ private void UpdateSelectedInfo()
+ {
+ var info = _mainMenuControl.Info;
+
+ if (_selected is not { } replay)
+ {
+ info.SetMarkup(Loc.GetString("replay-info-none-selected"));
+ info.HorizontalAlignment = Control.HAlignment.Center;
+ info.VerticalAlignment = Control.VAlignment.Center;
+ _mainMenuControl.LoadButton.Disabled = true;
+ return;
+ }
+
+ if (!_resMan.UserData.Exists(replay)
+ || _loadMan.LoadYamlMetadata(_resMan.UserData, replay) is not { } data)
+ {
+ info.SetMarkup(Loc.GetString("replay-info-invalid"));
+ info.HorizontalAlignment = Control.HAlignment.Center;
+ info.VerticalAlignment = Control.VAlignment.Center;
+ _mainMenuControl.LoadButton.Disabled = true;
+ return;
+ }
+
+ var file = replay.ToRelativePath().ToString();
+ data.TryGet(Time, out var timeNode);
+ data.TryGet(Duration, out var durationNode);
+ data.TryGet("roundId", out var roundIdNode);
+ data.TryGet(Hash, out var hashNode);
+ data.TryGet(CompHash, out var compHashNode);
+ DateTime.TryParse(timeNode?.Value, out var time);
+ TimeSpan.TryParse(durationNode?.Value, out var duration);
+
+ var forkId = string.Empty;
+ if (data.TryGet(Fork, out var forkNode))
+ {
+ // TODO REPLAYS somehow distribute and load from build.json?
+ var clientFork = _cfg.GetCVar(CVars.BuildForkId);
+ if (string.IsNullOrWhiteSpace(clientFork))
+ forkId = forkNode.Value;
+ else if (forkNode.Value == clientFork)
+ forkId = $"[color=green]{forkNode.Value}[/color]";
+ else
+ forkId = $"[color=yellow]{forkNode.Value}[/color]";
+ }
+
+ var forkVersion = string.Empty;
+ if (data.TryGet(ForkVersion, out var versionNode))
+ {
+ forkVersion = versionNode.Value;
+ // Why does this not have a try-convert function? I just want to check if it looks like a hash code.
+ try
+ {
+ Convert.FromHexString(forkVersion);
+ // version is a probably some git commit. Crop it to keep the info box small.
+ forkVersion = forkVersion[..16];
+ }
+ catch
+ {
+ // ignored
+ }
+
+ // TODO REPLAYS somehow distribute and load from build.json?
+ var clientVer = _cfg.GetCVar(CVars.BuildVersion);
+ if (!string.IsNullOrWhiteSpace(clientVer))
+ {
+ if (versionNode.Value == clientVer)
+ forkVersion = $"[color=green]{forkVersion}[/color]";
+ else
+ forkVersion = $"[color=yellow]{forkVersion}[/color]";
+ }
+ }
+
+ if (hashNode == null)
+ throw new Exception("Invalid metadata file. Missing type hash");
+
+ var typeHash = hashNode.Value;
+ _mainMenuControl.LoadButton.Disabled = false;
+ if (Convert.FromHexString(typeHash).SequenceEqual(_serializer.GetSerializableTypesHash()))
+ {
+ typeHash = $"[color=green]{typeHash[..16]}[/color]";
+ }
+ else
+ {
+ typeHash = $"[color=red]{typeHash[..16]}[/color]";
+ _mainMenuControl.LoadButton.Disabled = true;
+ }
+
+ if (compHashNode == null)
+ throw new Exception("Invalid metadata file. Missing component hash");
+
+ var compHash = compHashNode.Value;
+ if (Convert.FromHexString(compHash).SequenceEqual(_factory.GetHash(true)))
+ {
+ compHash = $"[color=green]{compHash[..16]}[/color]";
+ _mainMenuControl.LoadButton.Disabled = false;
+ }
+ else
+ {
+ compHash = $"[color=red]{compHash[..16]}[/color]";
+ _mainMenuControl.LoadButton.Disabled = true;
+ }
+
+ var engineVersion = string.Empty;
+ if (data.TryGet(Engine, out var engineNode))
+ {
+ var clientVer = _cfg.GetCVar(CVars.BuildEngineVersion);
+ if (string.IsNullOrWhiteSpace(clientVer))
+ engineVersion = engineNode.Value;
+ else if (engineNode.Value == clientVer)
+ engineVersion = $"[color=green]{engineNode.Value}[/color]";
+ else
+ engineVersion = $"[color=yellow]{engineNode.Value}[/color]";
+ }
+
+ // Strip milliseconds. Apparently there is no general format string that suppresses milliseconds.
+ duration = new((int)Math.Floor(duration.TotalDays), duration.Hours, duration.Minutes, duration.Seconds);
+
+ data.TryGet(Name, out var nameNode);
+ var name = nameNode?.Value ?? string.Empty;
+
+ info.HorizontalAlignment = Control.HAlignment.Left;
+ info.VerticalAlignment = Control.VAlignment.Top;
+ info.SetMarkup(Loc.GetString(
+ "replay-info-info",
+ ("file", file),
+ ("name", name),
+ ("time", time),
+ ("roundId", roundIdNode?.Value ?? "???"),
+ ("duration", duration),
+ ("forkId", forkId),
+ ("version", forkVersion),
+ ("engVersion", engineVersion),
+ ("compHash", compHash),
+ ("hash", typeHash)));
+ }
+
+ private void OnFolderPressed(BaseButton.ButtonEventArgs obj)
+ {
+ _resMan.UserData.CreateDir(_directory);
+ _resMan.UserData.OpenOsWindow(_directory);
+ }
+
+ private void OnLoadpressed(BaseButton.ButtonEventArgs obj)
+ {
+ if (_selected.HasValue)
+ _loadMan.LoadAndStartReplay(_resMan.UserData, _selected.Value);
+ }
+
+ private void RefreshReplays()
+ {
+ _replays.Clear();
+
+ foreach (var entry in _resMan.UserData.DirectoryEntries(_directory))
+ {
+ var file = _directory / entry;
+ try
+ {
+ var data = _loadMan.LoadYamlMetadata(_resMan.UserData, file);
+ if (data == null)
+ continue;
+
+ var name = data.Get(Name).Value;
+ _replays.Add((name, file));
+
+ }
+ catch
+ {
+ // Ignore file
+ }
+ }
+
+ _selectWindow?.Repopulate(_replays);
+ if (_selected.HasValue && _replays.All(x => x.Path != _selected.Value))
+ SelectReplay(null);
+ else
+ _selectWindow?.UpdateSelected(_selected);
+ }
+
+ public void SelectReplay(ResPath? replay)
+ {
+ if (_selected == replay)
+ return;
+
+ _selected = replay;
+ try
+ {
+ UpdateSelectedInfo();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Failed to load replay info. Exception: {ex}");
+ SelectReplay(null);
+ return;
+ }
+ _selectWindow?.UpdateSelected(replay);
+ }
+
+ protected override void Shutdown()
+ {
+ _mainMenuControl.Dispose();
+ _selectWindow?.Dispose();
+ }
+
+ private void OptionsButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ _userInterfaceManager.GetUIController().ToggleWindow();
+ }
+
+ private void QuitButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ _controllerProxy.Shutdown();
+ }
+
+ private void OnSelectPressed(BaseButton.ButtonEventArgs args)
+ {
+ RefreshReplays();
+ _selectWindow ??= new(this);
+ _selectWindow.Repopulate(_replays);
+ _selectWindow.UpdateSelected(_selected);
+ _selectWindow.OpenCentered();
+ }
+}
diff --git a/Content.Replay/Menu/ReplayMainMenuControl.xaml b/Content.Replay/Menu/ReplayMainMenuControl.xaml
new file mode 100644
index 0000000000..f26db1121f
--- /dev/null
+++ b/Content.Replay/Menu/ReplayMainMenuControl.xaml
@@ -0,0 +1,52 @@
+
+
+
+