diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b6d556117..4c5dd76940 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,7 +19,7 @@ /Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer /Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer -/Resources/Prototypes/Maps/ @Emisse +/Resources/Prototypes/Maps/** @Emisse /Resources/Prototypes/Body/ @DrSmugleaf # suffering /Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf diff --git a/.github/workflows/labeler-stable.yml b/.github/workflows/labeler-stable.yml new file mode 100644 index 0000000000..491d6a76fa --- /dev/null +++ b/.github/workflows/labeler-stable.yml @@ -0,0 +1,16 @@ +name: "Labels: Branch stable" + +on: + pull_request_target: + types: + - opened + branches: + - 'stable' + +jobs: + add_label: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-add-labels@v1 + with: + labels: "Branch: stable" diff --git a/.github/workflows/labeler-staging.yml b/.github/workflows/labeler-staging.yml new file mode 100644 index 0000000000..e31a5a482f --- /dev/null +++ b/.github/workflows/labeler-staging.yml @@ -0,0 +1,16 @@ +name: "Labels: Branch staging" + +on: + pull_request_target: + types: + - opened + branches: + - 'staging' + +jobs: + add_label: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-add-labels@v1 + with: + labels: "Branch: staging" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d08ae13502..77cf56a512 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,8 +5,8 @@ concurrency: on: workflow_dispatch: - schedule: - - cron: '0 10 * * *' + # schedule: + # - cron: '0 10 * * *' jobs: build: diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index 525ef1f018..c5ec254c0c 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Shared.Alert; using JetBrains.Annotations; using Robust.Client.Player; +using Robust.Shared.GameStates; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -24,8 +25,7 @@ public sealed class ClientAlertsSystem : AlertsSystem SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - - SubscribeLocalEvent(ClientAlertsHandleState); + SubscribeLocalEvent(OnHandleState); } protected override void LoadPrototypes() { @@ -47,6 +47,16 @@ public sealed class ClientAlertsSystem : AlertsSystem } } + private void OnHandleState(Entity alerts, ref ComponentHandleState args) + { + if (args.Current is not AlertComponentState cast) + return; + + alerts.Comp.Alerts = cast.Alerts; + + UpdateHud(alerts); + } + protected override void AfterShowAlert(Entity alerts) { UpdateHud(alerts); @@ -57,11 +67,6 @@ public sealed class ClientAlertsSystem : AlertsSystem UpdateHud(alerts); } - private void ClientAlertsHandleState(Entity alerts, ref AfterAutoHandleStateEvent args) - { - UpdateHud(alerts); - } - private void UpdateHud(Entity entity) { if (_playerManager.LocalEntity == entity.Owner) diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs index f0b7ffbe11..81c9a409a3 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow { private readonly IEntityManager _entManager; private readonly SpriteSystem _spriteSystem; + private readonly SharedNavMapSystem _navMapSystem; private EntityUid? _owner; private NetEntity? _trackedEntity; @@ -42,19 +43,32 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow private const float SilencingDuration = 2.5f; + // Colors + private Color _wallColor = new Color(64, 64, 64); + private Color _tileColor = new Color(28, 28, 28); + private Color _monitorBlipColor = Color.Cyan; + private Color _untrackedEntColor = Color.DimGray; + private Color _regionBaseColor = new Color(154, 154, 154); + private Color _inactiveColor = StyleNano.DisabledFore; + private Color _statusTextColor = StyleNano.GoodGreenFore; + private Color _goodColor = Color.LimeGreen; + private Color _warningColor = new Color(255, 182, 72); + private Color _dangerColor = new Color(255, 67, 67); + public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner) { RobustXamlLoader.Load(this); _entManager = IoCManager.Resolve(); _spriteSystem = _entManager.System(); + _navMapSystem = _entManager.System(); // Pass the owner to nav map _owner = owner; NavMap.Owner = _owner; // Set nav map colors - NavMap.WallColor = new Color(64, 64, 64); - NavMap.TileColor = Color.DimGray * NavMap.WallColor; + NavMap.WallColor = _wallColor; + NavMap.TileColor = _tileColor; // Set nav map grid uid var stationName = Loc.GetString("atmos-alerts-window-unknown-location"); @@ -179,6 +193,9 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow // Add tracked entities to the nav map foreach (var device in console.AtmosDevices) { + if (!device.NetEntity.Valid) + continue; + if (!NavMap.Visible) continue; @@ -209,7 +226,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow if (consoleCoords != null && consoleUid != null) { var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); - var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false); + var blip = new NavMapBlip(consoleCoords.Value, texture, _monitorBlipColor, true, false); NavMap.TrackedEntities[consoleUid.Value] = blip; } @@ -258,7 +275,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow VerticalAlignment = VAlignment.Center, }; - label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha()))); + label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", _statusTextColor.ToHexNoAlpha()))); AlertsTable.AddChild(label); } @@ -270,6 +287,34 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow else MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount))); + // Update sensor regions + NavMap.RegionOverlays.Clear(); + var prioritizedRegionOverlays = new Dictionary(); + + if (_owner != null && + _entManager.TryGetComponent(_owner, out var xform) && + _entManager.TryGetComponent(xform.GridUid, out var navMap)) + { + var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key); + + foreach (var (regionOwner, regionOverlay) in regionOverlays) + { + var alarmState = GetAlarmState(regionOwner); + + if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor)) + continue; + + regionOverlay.Color = regionColor; + + var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState; + prioritizedRegionOverlays.Add(regionOverlay, priority); + } + + // Sort overlays according to their priority + var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList(); + NavMap.RegionOverlays = sortedOverlays; + } + // Auto-scroll re-enable if (_autoScrollAwaitsUpdate) { @@ -290,7 +335,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow var coords = _entManager.GetCoordinates(metaData.NetCoordinates); if (_trackedEntity != null && _trackedEntity != metaData.NetEntity) - color *= Color.DimGray; + color *= _untrackedEntColor; var selectable = true; var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable); @@ -298,6 +343,24 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow NavMap.TrackedEntities[metaData.NetEntity] = blip; } + private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color) + { + color = Color.White; + + var blip = GetBlipTexture(alarmState); + + if (blip == null) + return false; + + // Color the region based on alarm state and entity tracking + color = blip.Value.Item2 * _regionBaseColor; + + if (_trackedEntity != null && _trackedEntity != regionOwner) + color *= _untrackedEntColor; + + return true; + } + private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null) { // Make new UI entry if required @@ -534,13 +597,13 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow switch (alarmState) { case AtmosAlarmType.Invalid: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _inactiveColor); break; case AtmosAlarmType.Normal: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break; case AtmosAlarmType.Warning: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break; case AtmosAlarmType.Danger: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break; } return output; diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs index 9864dbcb2a..7d7d77f51a 100644 --- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs @@ -20,7 +20,6 @@ public sealed partial class ContentAudioSystem { [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly ClientGameTicker _gameTicker = default!; - [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; private readonly AudioParams _lobbySoundtrackParams = new(-5f, 1, 0, 0, 0, false, 0f); @@ -71,7 +70,7 @@ public sealed partial class ContentAudioSystem Subs.CVar(_configManager, CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged); Subs.CVar(_configManager, CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged); - _stateManager.OnStateChanged += StateManagerOnStateChanged; + _state.OnStateChanged += StateManagerOnStateChanged; _client.PlayerLeaveServer += OnLeave; @@ -115,7 +114,7 @@ public sealed partial class ContentAudioSystem private void LobbyMusicCVarChanged(bool musicEnabled) { - if (musicEnabled && _stateManager.CurrentState is LobbyState) + if (musicEnabled && _state.CurrentState is LobbyState) { StartLobbyMusic(); } @@ -234,7 +233,7 @@ public sealed partial class ContentAudioSystem private void ShutdownLobbyMusic() { - _stateManager.OnStateChanged -= StateManagerOnStateChanged; + _state.OnStateChanged -= StateManagerOnStateChanged; _client.PlayerLeaveServer -= OnLeave; diff --git a/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs b/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs new file mode 100644 index 0000000000..f173932478 --- /dev/null +++ b/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Timing; +using Content.Shared.Cargo.Systems; + +namespace Content.Client.Cargo.Systems; + +/// +/// This handles... +/// +public sealed class ClientPriceGunSystem : SharedPriceGunSystem +{ + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user) + { + if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay))) + return false; + + // It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server. + return true; + } +} diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index e428d30f20..68707e021c 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -21,6 +21,16 @@ internal sealed class ChatManager : IChatManager _sawmill.Level = LogLevel.Info; } + public void SendAdminAlert(string message) + { + // See server-side manager. This just exists for shared code. + } + + public void SendAdminAlert(EntityUid player, string message) + { + // See server-side manager. This just exists for shared code. + } + public void SendMessage(string text, ChatSelectChannel channel) { var str = text.ToString(); diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index 6464ca1019..62a97c6bd8 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -2,10 +2,8 @@ using Content.Shared.Chat; namespace Content.Client.Chat.Managers { - public interface IChatManager + public interface IChatManager : ISharedChatManager { - void Initialize(); - public void SendMessage(string text, ChatSelectChannel channel); } } diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 27d77eda49..3462fc9236 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -320,7 +320,8 @@ public sealed class ClientClothingSystem : ClothingSystem if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId)) continue; - _displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers); + if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers)) + index++; } } diff --git a/Content.Client/Commands/ShowHealthBarsCommand.cs b/Content.Client/Commands/ShowHealthBarsCommand.cs index 0811f96663..6ea9d06c8c 100644 --- a/Content.Client/Commands/ShowHealthBarsCommand.cs +++ b/Content.Client/Commands/ShowHealthBarsCommand.cs @@ -1,6 +1,8 @@ +using Content.Shared.Damage.Prototypes; using Content.Shared.Overlays; using Robust.Client.Player; using Robust.Shared.Console; +using Robust.Shared.Prototypes; using System.Linq; namespace Content.Client.Commands; @@ -34,7 +36,7 @@ public sealed class ShowHealthBarsCommand : LocalizedCommands { var showHealthBarsComponent = new ShowHealthBarsComponent { - DamageContainers = args.ToList(), + DamageContainers = args.Select(arg => new ProtoId(arg)).ToList(), HealthStatusIcon = null, NetSyncEnabled = false }; diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index 956c946524..b584aa9ad1 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -124,6 +124,10 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem continue; } + var targetEv = new GetFlashEffectTargetEvent(ent); + RaiseLocalEvent(ent, ref targetEv); + ent = targetEv.Target; + EnsureComp(ent, out comp); comp.NetSyncEnabled = false; comp.Color = sprite.Color; @@ -132,3 +136,9 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem } } } + +/// +/// Raised on an entity to change the target for a color flash effect. +/// +[ByRefEvent] +public record struct GetFlashEffectTargetEvent(EntityUid Target); diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index c4c18f154a..5c1f94f333 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -4,6 +4,7 @@ using Content.Client.Chat.Managers; using Content.Client.DebugMon; using Content.Client.Eui; using Content.Client.Fullscreen; +using Content.Client.GameTicking.Managers; using Content.Client.GhostKick; using Content.Client.Guidebook; using Content.Client.Input; @@ -69,8 +70,8 @@ namespace Content.Client.Entry [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly IReplayLoadManager _replayLoad = default!; [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly ContentReplayPlaybackManager _replayMan = default!; [Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!; + [Dependency] private readonly TitleWindowManager _titleWindowManager = default!; public override void Init() { @@ -140,6 +141,12 @@ namespace Content.Client.Entry _configManager.SetCVar("interface.resolutionAutoScaleMinimum", 0.5f); } + public override void Shutdown() + { + base.Shutdown(); + _titleWindowManager.Shutdown(); + } + public override void PostInit() { base.PostInit(); @@ -160,6 +167,7 @@ namespace Content.Client.Entry _userInterfaceManager.SetDefaultTheme("SS14DefaultTheme"); _userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme)); _documentParsingManager.Initialize(); + _titleWindowManager.Initialize(); _baseClient.RunLevelChanged += (_, args) => { @@ -191,7 +199,7 @@ namespace Content.Client.Entry _resourceManager, ReplayConstants.ReplayZipFolder.ToRootedPath()); - _replayMan.LastLoad = (null, ReplayConstants.ReplayZipFolder.ToRootedPath()); + _playbackMan.LastLoad = (null, ReplayConstants.ReplayZipFolder.ToRootedPath()); _replayLoad.LoadAndStartReplay(reader); } else if (_gameController.LaunchState.FromLauncher) diff --git a/Content.Client/GameTicking/Managers/TitleWindowManager.cs b/Content.Client/GameTicking/Managers/TitleWindowManager.cs new file mode 100644 index 0000000000..18ce16f634 --- /dev/null +++ b/Content.Client/GameTicking/Managers/TitleWindowManager.cs @@ -0,0 +1,62 @@ +using Content.Shared.CCVar; +using Robust.Client; +using Robust.Client.Graphics; +using Robust.Shared; +using Robust.Shared.Configuration; + +namespace Content.Client.GameTicking.Managers; + +public sealed class TitleWindowManager +{ + [Dependency] private readonly IBaseClient _client = default!; + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IGameController _gameController = default!; + + public void Initialize() + { + _cfg.OnValueChanged(CVars.GameHostName, OnHostnameChange, true); + _cfg.OnValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange, true); + + _client.RunLevelChanged += OnRunLevelChangedChange; + } + + public void Shutdown() + { + _cfg.UnsubValueChanged(CVars.GameHostName, OnHostnameChange); + _cfg.UnsubValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange); + } + + private void OnHostnameChange(string hostname) + { + var defaultWindowTitle = _gameController.GameTitle(); + + // Since the game assumes the server name is MyServer and that GameHostnameInTitlebar CCVar is true by default + // Lets just... not show anything. This also is used to revert back to just the game title on disconnect. + if (_client.RunLevel == ClientRunLevel.Initialize) + { + _clyde.SetWindowTitle(defaultWindowTitle); + return; + } + + if (_cfg.GetCVar(CCVars.GameHostnameInTitlebar)) + // If you really dislike the dash I guess change it here + _clyde.SetWindowTitle(hostname + " - " + defaultWindowTitle); + else + _clyde.SetWindowTitle(defaultWindowTitle); + } + + // Clients by default assume game.hostname_in_titlebar is true + // but we need to clear it as soon as we join and actually receive the servers preference on this. + // This will ensure we rerun OnHostnameChange and set the correct title bar name. + private void OnHostnameTitleChange(bool colonthree) + { + OnHostnameChange(_cfg.GetCVar(CVars.GameHostName)); + } + + // This is just used we can rerun the hostname change function when we disconnect to revert back to just the games title. + private void OnRunLevelChangedChange(object? sender, RunLevelChangedEventArgs runLevelChangedEventArgs) + { + OnHostnameChange(_cfg.GetCVar(CVars.GameHostName)); + } +} diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index ffa6dfd29d..68800a2afe 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -130,9 +130,9 @@ namespace Content.Client.Hands.Systems OnPlayerHandsAdded?.Invoke(hands); } - public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null) + public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true) { - base.DoDrop(uid, hand, doDropInteraction, hands); + base.DoDrop(uid, hand, doDropInteraction, hands, log); if (TryComp(hand.HeldEntity, out SpriteComponent? sprite)) sprite.RenderOrder = EntityManager.CurrentTick.Value; diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index 19d00a0bbf..aae8785b1f 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -47,8 +47,7 @@ - + diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index d61267d002..fd3615d59f 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -110,18 +110,29 @@ namespace Content.Client.HealthAnalyzer.UI // Alerts - AlertsDivider.Visible = msg.Bleeding == true; - AlertsContainer.Visible = msg.Bleeding == true; + var showAlerts = msg.Unrevivable == true || msg.Bleeding == true; + + AlertsDivider.Visible = showAlerts; + AlertsContainer.Visible = showAlerts; + + if (showAlerts) + AlertsContainer.DisposeAllChildren(); + + if (msg.Unrevivable == true) + AlertsContainer.AddChild(new RichTextLabel + { + Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"), + Margin = new Thickness(0, 4), + MaxWidth = 300 + }); if (msg.Bleeding == true) - { - AlertsContainer.DisposeAllChildren(); - AlertsContainer.AddChild(new Label + AlertsContainer.AddChild(new RichTextLabel { Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"), - FontColorOverride = Color.Red, + Margin = new Thickness(0, 4), + MaxWidth = 300 }); - } // Damage Groups diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs index aff01800f9..632ad8de4a 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs @@ -1,3 +1,4 @@ +using Content.Shared.Localizations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; @@ -16,19 +17,10 @@ public sealed partial class PlaytimeStatsEntry : ContainerButton RoleLabel.Text = role; Playtime = playtime; // store the TimeSpan value directly - PlaytimeLabel.Text = ConvertTimeSpanToHoursMinutes(playtime); // convert to string for display + PlaytimeLabel.Text = ContentLocalizationManager.FormatPlaytime(playtime); // convert to string for display BackgroundColorPanel.PanelOverride = styleBox; } - private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan) - { - var hours = (int)timeSpan.TotalHours; - var minutes = timeSpan.Minutes; - - var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes)); - return formattedTimeLoc; - } - public void UpdateShading(StyleBoxFlat styleBox) { BackgroundColorPanel.PanelOverride = styleBox; diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs index 3b54bf82da..98241b2cca 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs @@ -104,8 +104,7 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow { var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime(); - var formattedPlaytime = ConvertTimeSpanToHoursMinutes(overallPlaytime); - OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", formattedPlaytime)); + OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", overallPlaytime)); var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles(); @@ -134,13 +133,4 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow _sawmill.Error($"The provided playtime string '{playtimeString}' is not in the correct format."); } } - - private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan) - { - var hours = (int) timeSpan.TotalHours; - var minutes = timeSpan.Minutes; - - var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes)); - return formattedTimeLoc; - } } diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 97172f8de8..2ce07758c9 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -98,7 +98,7 @@ namespace Content.Client.Inventory } } - if (EntMan.TryGetComponent(Owner, out var handsComp)) + if (EntMan.TryGetComponent(Owner, out var handsComp) && handsComp.CanBeStripped) { // good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands // and not gui hands... which are different... diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 1fd237cf3e..370188e3c6 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -5,6 +5,7 @@ using Content.Client.Clickable; using Content.Client.DebugMon; using Content.Client.Eui; using Content.Client.Fullscreen; +using Content.Client.GameTicking.Managers; using Content.Client.GhostKick; using Content.Client.Guidebook; using Content.Client.Launcher; @@ -18,8 +19,11 @@ using Content.Client.Viewport; using Content.Client.Voting; using Content.Shared.Administration.Logs; using Content.Client.Lobby; +using Content.Client.Players.RateLimiting; using Content.Shared.Administration.Managers; +using Content.Shared.Chat; using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Players.RateLimiting; namespace Content.Client.IoC { @@ -31,6 +35,7 @@ namespace Content.Client.IoC collection.Register(); collection.Register(); + collection.Register(); collection.Register(); collection.Register(); collection.Register(); @@ -47,10 +52,13 @@ namespace Content.Client.IoC collection.Register(); collection.Register(); collection.Register(); - collection.Register(); + collection.Register(); collection.Register(); collection.Register(); collection.Register(); + collection.Register(); + collection.Register(); + collection.Register(); } } } diff --git a/Content.Client/Mapping/MappingScreen.xaml b/Content.Client/Mapping/MappingScreen.xaml index 9cc3e734f0..bad492e7e4 100644 --- a/Content.Client/Mapping/MappingScreen.xaml +++ b/Content.Client/Mapping/MappingScreen.xaml @@ -8,7 +8,7 @@ VerticalExpand="False" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - @@ -82,5 +82,5 @@ - + diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs index 46c0e51fad..20e2528a44 100644 --- a/Content.Client/Mapping/MappingScreen.xaml.cs +++ b/Content.Client/Mapping/MappingScreen.xaml.cs @@ -197,7 +197,6 @@ public sealed partial class MappingScreen : InGameScreen public override void SetChatSize(Vector2 size) { - ScreenContainer.DesiredSplitCenter = size.X; ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize; } diff --git a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs deleted file mode 100644 index 0ed2d04f69..0000000000 --- a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Numerics; -using Content.Client.Buckle; -using Content.Client.Gravity; -using Content.Shared.ActionBlocker; -using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Robust.Client.Animations; -using Robust.Client.GameObjects; -using Robust.Shared.Animations; - -namespace Content.Client.Movement.Systems; - -public sealed class WaddleAnimationSystem : SharedWaddleAnimationSystem -{ - [Dependency] private readonly AnimationPlayerSystem _animation = default!; - [Dependency] private readonly GravitySystem _gravity = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly BuckleSystem _buckle = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeAllEvent(OnStartWaddling); - SubscribeLocalEvent(OnAnimationCompleted); - SubscribeAllEvent(OnStopWaddling); - } - - private void OnStartWaddling(StartedWaddlingEvent msg, EntitySessionEventArgs args) - { - if (TryComp(GetEntity(msg.Entity), out var comp)) - StartWaddling((GetEntity(msg.Entity), comp)); - } - - private void OnStopWaddling(StoppedWaddlingEvent msg, EntitySessionEventArgs args) - { - if (TryComp(GetEntity(msg.Entity), out var comp)) - StopWaddling((GetEntity(msg.Entity), comp)); - } - - private void StartWaddling(Entity entity) - { - if (_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName)) - return; - - if (!TryComp(entity.Owner, out var mover)) - return; - - if (_gravity.IsWeightless(entity.Owner)) - return; - - if (!_actionBlocker.CanMove(entity.Owner, mover)) - return; - - // Do nothing if buckled in - if (_buckle.IsBuckled(entity.Owner)) - return; - - // Do nothing if crit or dead (for obvious reasons) - if (_mobState.IsIncapacitated(entity.Owner)) - return; - - PlayWaddleAnimationUsing( - (entity.Owner, entity.Comp), - CalculateAnimationLength(entity.Comp, mover), - CalculateTumbleIntensity(entity.Comp) - ); - } - - private static float CalculateTumbleIntensity(WaddleAnimationComponent component) - { - return component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity; - } - - private static float CalculateAnimationLength(WaddleAnimationComponent component, InputMoverComponent mover) - { - return mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength; - } - - private void OnAnimationCompleted(Entity entity, ref AnimationCompletedEvent args) - { - if (args.Key != entity.Comp.KeyName) - return; - - if (!TryComp(entity.Owner, out var mover)) - return; - - PlayWaddleAnimationUsing( - (entity.Owner, entity.Comp), - CalculateAnimationLength(entity.Comp, mover), - CalculateTumbleIntensity(entity.Comp) - ); - } - - private void StopWaddling(Entity entity) - { - if (!_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName)) - return; - - _animation.Stop(entity.Owner, entity.Comp.KeyName); - - if (!TryComp(entity.Owner, out var sprite)) - return; - - sprite.Offset = new Vector2(); - sprite.Rotation = Angle.FromDegrees(0); - } - - private void PlayWaddleAnimationUsing(Entity entity, float len, float tumbleIntensity) - { - entity.Comp.LastStep = !entity.Comp.LastStep; - - var anim = new Animation() - { - Length = TimeSpan.FromSeconds(len), - AnimationTracks = - { - new AnimationTrackComponentProperty() - { - ComponentType = typeof(SpriteComponent), - Property = nameof(SpriteComponent.Rotation), - InterpolationMode = AnimationInterpolationMode.Linear, - KeyFrames = - { - new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0), - new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(tumbleIntensity), len/2), - new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), len/2), - } - }, - new AnimationTrackComponentProperty() - { - ComponentType = typeof(SpriteComponent), - Property = nameof(SpriteComponent.Offset), - InterpolationMode = AnimationInterpolationMode.Linear, - KeyFrames = - { - new AnimationTrackProperty.KeyFrame(new Vector2(), 0), - new AnimationTrackProperty.KeyFrame(entity.Comp.HopIntensity, len/2), - new AnimationTrackProperty.KeyFrame(new Vector2(), len/2), - } - } - } - }; - - _animation.Play(entity.Owner, anim, entity.Comp.KeyName); - } -} diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs index 44be1f7a67..9fc64165c5 100644 --- a/Content.Client/Overlays/ShowThirstIconsSystem.cs +++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs @@ -22,6 +22,6 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem entity, ushort subTick, bool walking) + { + // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}"); + base.SetSprinting(entity, subTick, walking); + + if (walking && _cfg.GetCVar(CCVars.ToggleWalk)) + _alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false); + else + _alerts.ClearAlert(entity, WalkingAlert); + } } diff --git a/Content.Client/Pinpointer/NavMapSystem.Regions.cs b/Content.Client/Pinpointer/NavMapSystem.Regions.cs new file mode 100644 index 0000000000..4cc775418e --- /dev/null +++ b/Content.Client/Pinpointer/NavMapSystem.Regions.cs @@ -0,0 +1,303 @@ +using Content.Shared.Atmos; +using Content.Shared.Pinpointer; +using System.Linq; + +namespace Content.Client.Pinpointer; + +public sealed partial class NavMapSystem +{ + private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable = + { + (AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West), + (AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East), + (AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South), + (AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North), + }; + + public override void Update(float frameTime) + { + // To prevent compute spikes, only one region is flood filled per frame + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entNavMapRegions)) + FloodFillNextEnqueuedRegion(ent, entNavMapRegions); + } + + private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component) + { + if (!component.QueuedRegionsToFlood.Any()) + return; + + var regionOwner = component.QueuedRegionsToFlood.Dequeue(); + + // If the region is no longer valid, flood the next one in the queue + if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) || + !regionProperties.Seeds.Any()) + { + FloodFillNextEnqueuedRegion(uid, component); + return; + } + + // Flood fill the region, using the region seeds as starting points + var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties); + + // Combine the flooded tiles into larger rectangles + var gridCoords = GetMergedRegionTiles(floodedTiles); + + // Create and assign the new region overlay + var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords) + { + Color = regionProperties.Color + }; + + component.RegionOverlays[regionOwner] = regionOverlay; + + // To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner + + // First remove an old assignments + if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks)) + { + foreach (var chunk in oldChunks) + { + if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners)) + { + oldOwners.Remove(regionOwner); + component.ChunkToRegionOwnerTable[chunk] = oldOwners; + } + } + } + + // Now update with the new assignments + component.RegionOwnerToChunkTable[regionOwner] = floodedChunks; + + foreach (var chunk in floodedChunks) + { + if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners)) + owners = new(); + + owners.Add(regionOwner); + component.ChunkToRegionOwnerTable[chunk] = owners; + } + } + + private (HashSet, HashSet) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties) + { + if (!regionProperties.Seeds.Any()) + return (new(), new()); + + var visitedChunks = new HashSet(); + var visitedTiles = new HashSet(); + var tilesToVisit = new Stack(); + + foreach (var regionSeed in regionProperties.Seeds) + { + tilesToVisit.Push(regionSeed); + + while (tilesToVisit.Count > 0) + { + // If the max region area is hit, exit + if (visitedTiles.Count > regionProperties.MaxArea) + return (new(), new()); + + // Pop the top tile from the stack + var current = tilesToVisit.Pop(); + + // If the current tile position has already been visited, + // or is too far away from the seed, continue + if ((regionSeed - current).Length > regionProperties.MaxRadius) + continue; + + if (visitedTiles.Contains(current)) + continue; + + // Determine the tile's chunk index + var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize); + var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize); + var idx = GetTileIndex(relative); + + // Extract the tile data + if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk)) + continue; + + var flag = chunk.TileData[idx]; + + // If the current tile is entirely occupied, continue + if ((FloorMask & flag) == 0) + continue; + + if ((WallMask & flag) == WallMask) + continue; + + if ((AirlockMask & flag) == AirlockMask) + continue; + + // Otherwise the tile can be added to this region + visitedTiles.Add(current); + visitedChunks.Add(chunkOrigin); + + // Determine if we can propagate the region into its cardinally adjacent neighbors + // To propagate to a neighbor, movement into the neighbors closest edge must not be + // blocked, and vice versa + + foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable) + { + if (!RegionCanPropagateInDirection(chunk, current, direction)) + continue; + + var neighbor = current + tileOffset; + var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize); + + if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk)) + continue; + + visitedChunks.Add(neighborOrigin); + + if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection)) + continue; + + tilesToVisit.Push(neighbor); + } + } + } + + return (visitedTiles, visitedChunks); + } + + private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction) + { + var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize); + var idx = GetTileIndex(relative); + var flag = chunk.TileData[idx]; + + if ((FloorMask & flag) == 0) + return false; + + var directionMask = 1 << (int)direction; + var wallMask = (int)direction << (int)NavMapChunkType.Wall; + var airlockMask = (int)direction << (int)NavMapChunkType.Airlock; + + if ((wallMask & flag) > 0) + return false; + + if ((airlockMask & flag) > 0) + return false; + + return true; + } + + private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet tiles) + { + if (!tiles.Any()) + return new(); + + var x = tiles.Select(t => t.X); + var minX = x.Min(); + var maxX = x.Max(); + + var y = tiles.Select(t => t.Y); + var minY = y.Min(); + var maxY = y.Max(); + + var matrix = new int[maxX - minX + 1, maxY - minY + 1]; + + foreach (var tile in tiles) + { + var a = tile.X - minX; + var b = tile.Y - minY; + + matrix[a, b] = 1; + } + + return GetMergedRegionTiles(matrix, new Vector2i(minX, minY)); + } + + private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset) + { + var output = new List<(Vector2i, Vector2i)>(); + + var rows = matrix.GetLength(0); + var cols = matrix.GetLength(1); + + var dp = new int[rows, cols]; + var coords = (new Vector2i(), new Vector2i()); + var maxArea = 0; + + var count = 0; + + while (!IsArrayEmpty(matrix)) + { + count++; + + if (count > rows * cols) + break; + + // Clear old values + dp = new int[rows, cols]; + coords = (new Vector2i(), new Vector2i()); + maxArea = 0; + + // Initialize the first row of dp + for (int j = 0; j < cols; j++) + { + dp[0, j] = matrix[0, j]; + } + + // Calculate dp values for remaining rows + for (int i = 1; i < rows; i++) + { + for (int j = 0; j < cols; j++) + dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0; + } + + // Find the largest rectangular area seeded for each position in the matrix + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + int minWidth = dp[i, j]; + + for (int k = j; k >= 0; k--) + { + if (dp[i, k] <= 0) + break; + + minWidth = Math.Min(minWidth, dp[i, k]); + var currArea = Math.Max(maxArea, minWidth * (j - k + 1)); + + if (currArea > maxArea) + { + maxArea = currArea; + coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j)); + } + } + } + } + + // Save the recorded rectangle vertices + output.Add((coords.Item1 + offset, coords.Item2 + offset)); + + // Removed the tiles covered by the rectangle from matrix + for (int i = coords.Item1.X; i <= coords.Item2.X; i++) + { + for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++) + matrix[i, j] = 0; + } + } + + return output; + } + + private bool IsArrayEmpty(int[,] matrix) + { + for (int i = 0; i < matrix.GetLength(0); i++) + { + for (int j = 0; j < matrix.GetLength(1); j++) + { + if (matrix[i, j] == 1) + return false; + } + } + + return true; + } +} diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs index 9aeb792a42..47469d4ea7 100644 --- a/Content.Client/Pinpointer/NavMapSystem.cs +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Pinpointer; using Robust.Shared.GameStates; @@ -16,6 +17,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem { Dictionary modifiedChunks; Dictionary beacons; + Dictionary regions; switch (args.Current) { @@ -23,6 +25,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem { modifiedChunks = delta.ModifiedChunks; beacons = delta.Beacons; + regions = delta.Regions; + foreach (var index in component.Chunks.Keys) { if (!delta.AllChunks!.Contains(index)) @@ -35,6 +39,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem { modifiedChunks = state.Chunks; beacons = state.Beacons; + regions = state.Regions; + foreach (var index in component.Chunks.Keys) { if (!state.Chunks.ContainsKey(index)) @@ -47,13 +53,54 @@ public sealed partial class NavMapSystem : SharedNavMapSystem return; } + // Update region data and queue new regions for flooding + var prevRegionOwners = component.RegionProperties.Keys.ToList(); + var validRegionOwners = new List(); + + component.RegionProperties.Clear(); + + foreach (var (regionOwner, regionData) in regions) + { + if (!regionData.Seeds.Any()) + continue; + + component.RegionProperties[regionOwner] = regionData; + validRegionOwners.Add(regionOwner); + + if (component.RegionOverlays.ContainsKey(regionOwner)) + continue; + + if (component.QueuedRegionsToFlood.Contains(regionOwner)) + continue; + + component.QueuedRegionsToFlood.Enqueue(regionOwner); + } + + // Remove stale region owners + var regionOwnersToRemove = prevRegionOwners.Except(validRegionOwners); + + foreach (var regionOwnerRemoved in regionOwnersToRemove) + RemoveNavMapRegion(uid, component, regionOwnerRemoved); + + // Modify chunks foreach (var (origin, chunk) in modifiedChunks) { var newChunk = new NavMapChunk(origin); Array.Copy(chunk, newChunk.TileData, chunk.Length); component.Chunks[origin] = newChunk; + + // If the affected chunk intersects one or more regions, re-flood them + if (!component.ChunkToRegionOwnerTable.TryGetValue(origin, out var affectedOwners)) + continue; + + foreach (var affectedOwner in affectedOwners) + { + if (!component.QueuedRegionsToFlood.Contains(affectedOwner)) + component.QueuedRegionsToFlood.Enqueue(affectedOwner); + } } + // Refresh beacons component.Beacons.Clear(); foreach (var (nuid, beacon) in beacons) { diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 413b41c36a..90c2680c4a 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl public List<(Vector2, Vector2)> TileLines = new(); public List<(Vector2, Vector2)> TileRects = new(); public List<(Vector2[], Color)> TilePolygons = new(); + public List RegionOverlays = new(); // Default colors public Color WallColor = new(102, 217, 102); @@ -228,7 +229,7 @@ public partial class NavMapControl : MapGridControl { if (!blip.Selectable) continue; - + var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length(); if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance) @@ -319,6 +320,22 @@ public partial class NavMapControl : MapGridControl } } + // Draw region overlays + if (_grid != null) + { + foreach (var regionOverlay in RegionOverlays) + { + foreach (var gridCoords in regionOverlay.GridCoords) + { + var positionTopLeft = ScalePosition(new Vector2(gridCoords.Item1.X, -gridCoords.Item1.Y) - new Vector2(offset.X, -offset.Y)); + var positionBottomRight = ScalePosition(new Vector2(gridCoords.Item2.X + _grid.TileSize, -gridCoords.Item2.Y - _grid.TileSize) - new Vector2(offset.X, -offset.Y)); + + var box = new UIBox2(positionTopLeft, positionBottomRight); + handle.DrawRect(box, regionOverlay.Color); + } + } + } + // Draw map lines if (TileLines.Any()) { diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 6d52c50290..314b59eda9 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -51,6 +51,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager { // Reset on disconnect, just in case. _roles.Clear(); + _jobWhitelists.Clear(); + _roleBans.Clear(); } } @@ -58,9 +60,6 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager { _sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries."); - if (_roleBans.Equals(message.Bans)) - return; - _roleBans.Clear(); _roleBans.AddRange(message.Bans); Updated?.Invoke(); diff --git a/Content.Client/Players/RateLimiting/PlayerRateLimitManager.cs b/Content.Client/Players/RateLimiting/PlayerRateLimitManager.cs new file mode 100644 index 0000000000..e79eadd92b --- /dev/null +++ b/Content.Client/Players/RateLimiting/PlayerRateLimitManager.cs @@ -0,0 +1,23 @@ +using Content.Shared.Players.RateLimiting; +using Robust.Shared.Player; + +namespace Content.Client.Players.RateLimiting; + +public sealed class PlayerRateLimitManager : SharedPlayerRateLimitManager +{ + public override RateLimitStatus CountAction(ICommonSession player, string key) + { + // TODO Rate-Limit + // Add support for rate limit prediction + // I.e., dont mis-predict just because somebody is clicking too quickly. + return RateLimitStatus.Allowed; + } + + public override void Register(string key, RateLimitRegistration registration) + { + } + + public override void Initialize() + { + } +} diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs index 5ba4878c6d..8ba09c6617 100644 --- a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -1,7 +1,10 @@ +using Content.Client.Effects; +using Content.Client.Smoking; using Content.Shared.Chemistry.Components; using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; using Robust.Client.GameObjects; +using Robust.Shared.Player; namespace Content.Client.Polymorph.Systems; @@ -10,14 +13,20 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private EntityQuery _appearanceQuery; + private EntityQuery _spriteQuery; public override void Initialize() { base.Initialize(); _appearanceQuery = GetEntityQuery(); + _spriteQuery = GetEntityQuery(); SubscribeLocalEvent(OnHandleState); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetFlashEffectTargetEvent); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -25,9 +34,30 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem CopyComp(ent); CopyComp(ent); CopyComp(ent); + CopyComp(ent); // reload appearance to hopefully prevent any invisible layers if (_appearanceQuery.TryComp(ent, out var appearance)) _appearance.QueueUpdate(ent, appearance); } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + if (!_spriteQuery.TryComp(ent, out var sprite)) + return; + + ent.Comp.WasVisible = sprite.Visible; + sprite.Visible = false; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (_spriteQuery.TryComp(ent, out var sprite)) + sprite.Visible = ent.Comp.WasVisible; + } + + private void OnGetFlashEffectTargetEvent(Entity ent, ref GetFlashEffectTargetEvent args) + { + args.Target = ent.Comp.Disguise; + } } diff --git a/Content.Client/Power/APC/ApcBoundUserInterface.cs b/Content.Client/Power/APC/ApcBoundUserInterface.cs index a790c5d984..5c4036a915 100644 --- a/Content.Client/Power/APC/ApcBoundUserInterface.cs +++ b/Content.Client/Power/APC/ApcBoundUserInterface.cs @@ -1,8 +1,9 @@ -using Content.Client.Power.APC.UI; +using Content.Client.Power.APC.UI; +using Content.Shared.Access.Systems; using Content.Shared.APC; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; +using Robust.Shared.Player; namespace Content.Client.Power.APC { @@ -22,6 +23,14 @@ namespace Content.Client.Power.APC _menu = this.CreateWindow(); _menu.SetEntity(Owner); _menu.OnBreaker += BreakerPressed; + + var hasAccess = false; + if (PlayerManager.LocalEntity != null) + { + var accessReader = EntMan.System(); + hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner); + } + _menu?.SetAccessEnabled(hasAccess); } protected override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs index 2f61ea63a8..25e885b3c7 100644 --- a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs +++ b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs @@ -1,4 +1,4 @@ -using Robust.Client.AutoGenerated; +using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.XAML; using Robust.Client.GameObjects; using Robust.Shared.IoC; @@ -36,19 +36,9 @@ namespace Content.Client.Power.APC.UI { var castState = (ApcBoundInterfaceState) state; - if (BreakerButton != null) + if (!BreakerButton.Disabled) { - if(castState.HasAccess == false) - { - BreakerButton.Disabled = true; - BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access"); - } - else - { - BreakerButton.Disabled = false; - BreakerButton.ToolTip = null; - BreakerButton.Pressed = castState.MainBreaker; - } + BreakerButton.Pressed = castState.MainBreaker; } if (PowerLabel != null) @@ -86,6 +76,20 @@ namespace Content.Client.Power.APC.UI } } + public void SetAccessEnabled(bool hasAccess) + { + if(hasAccess) + { + BreakerButton.Disabled = false; + BreakerButton.ToolTip = null; + } + else + { + BreakerButton.Disabled = true; + BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access"); + } + } + private void UpdateChargeBarColor(float charge) { if (ChargeBar == null) diff --git a/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs b/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs deleted file mode 100644 index fd217bc7e8..0000000000 --- a/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Numerics; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.UserInterface.Controls; - -/// -/// A split container that performs an action when the split resizing is finished. -/// -public sealed class RecordedSplitContainer : SplitContainer -{ - public double? DesiredSplitCenter; - - protected override Vector2 ArrangeOverride(Vector2 finalSize) - { - if (ResizeMode == SplitResizeMode.RespectChildrenMinSize - && DesiredSplitCenter != null - && !finalSize.Equals(Vector2.Zero)) - { - SplitFraction = (float) DesiredSplitCenter.Value; - - if (!Size.Equals(Vector2.Zero)) - { - DesiredSplitCenter = null; - } - } - - return base.ArrangeOverride(finalSize); - } -} diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml index 7f1d1bcd5b..653302fae4 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml @@ -14,7 +14,7 @@ VerticalExpand="False" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - + @@ -26,7 +26,7 @@ - + @@ -36,5 +36,5 @@ - + diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs index e04d377d32..2892ca4425 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs @@ -40,7 +40,6 @@ public sealed partial class SeparatedChatGameScreen : InGameScreen public override void SetChatSize(Vector2 size) { - ScreenContainer.DesiredSplitCenter = size.X; ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize; } } diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 1dffeb8d2d..a6c1cfc94f 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -398,10 +398,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged (ActionButton) GetChild(index); } - private void BuildActionButtons(int count) + public void SetActionData(ActionsSystem system, params EntityUid?[] actionTypes) { + var uniqueCount = Math.Min(system.GetClientActions().Count(), actionTypes.Length + 1); var keys = ContentKeyFunctions.GetHotbarBoundKeys(); - Children.Clear(); - for (var index = 0; index < count; index++) + for (var i = 0; i < uniqueCount; i++) { - Children.Add(MakeButton(index)); + if (i >= ChildCount) + { + AddChild(MakeButton(i)); + } + + if (!actionTypes.TryGetValue(i, out var action)) + action = null; + ((ActionButton) GetChild(i)).UpdateData(action, system); + } + + for (var i = ChildCount - 1; i >= uniqueCount; i--) + { + RemoveChild(GetChild(i)); } ActionButton MakeButton(int index) @@ -55,20 +67,6 @@ public class ActionButtonContainer : GridContainer } } - public void SetActionData(ActionsSystem system, params EntityUid?[] actionTypes) - { - var uniqueCount = Math.Min(system.GetClientActions().Count(), actionTypes.Length + 1); - if (ChildCount != uniqueCount) - BuildActionButtons(uniqueCount); - - for (var i = 0; i < uniqueCount; i++) - { - if (!actionTypes.TryGetValue(i, out var action)) - action = null; - ((ActionButton) GetChild(i)).UpdateData(action, system); - } - } - public void ClearActionData() { foreach (var button in Children) diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml new file mode 100644 index 0000000000..32d611e771 --- /dev/null +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs similarity index 86% rename from Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs rename to Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs index fc53cc72ae..7df0243416 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs @@ -10,20 +10,17 @@ using Robust.Shared.Utility; namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles { [GenerateTypedNameReferences] - public sealed partial class GhostRolesEntry : BoxContainer + public sealed partial class GhostRoleButtonsBox : BoxContainer { private SpriteSystem _spriteSystem; public event Action? OnRoleSelected; public event Action? OnRoleFollow; - public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable roles, SpriteSystem spriteSystem) + public GhostRoleButtonsBox(bool hasAccess, FormattedMessage? reason, IEnumerable roles, SpriteSystem spriteSystem) { RobustXamlLoader.Load(this); _spriteSystem = spriteSystem; - Title.Text = name; - Description.SetMessage(description); - foreach (var role in roles) { var button = new GhostRoleEntryButtons(role); diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml index ffde5d69f7..05c52deef1 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml @@ -1,15 +1,15 @@  + Orientation="Horizontal" + HorizontalAlignment="Stretch">