Merge branch 'ion-storm-refactor' of https://github.com/ScarKy0/space-station-14 into ion-storm-refactor

This commit is contained in:
ScarKy0
2024-11-14 14:55:24 +01:00
1254 changed files with 59475 additions and 45491 deletions

2
.github/CODEOWNERS vendored
View File

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

16
.github/workflows/labeler-stable.yml vendored Normal file
View File

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

16
.github/workflows/labeler-staging.yml vendored Normal file
View File

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

View File

@@ -5,8 +5,8 @@ concurrency:
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * *'
# schedule:
# - cron: '0 10 * * *'
jobs:
build:

View File

@@ -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<AlertsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, AfterAutoHandleStateEvent>(ClientAlertsHandleState);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
}
protected override void LoadPrototypes()
{
@@ -47,6 +47,16 @@ public sealed class ClientAlertsSystem : AlertsSystem
}
}
private void OnHandleState(Entity<AlertsComponent> alerts, ref ComponentHandleState args)
{
if (args.Current is not AlertComponentState cast)
return;
alerts.Comp.Alerts = cast.Alerts;
UpdateHud(alerts);
}
protected override void AfterShowAlert(Entity<AlertsComponent> alerts)
{
UpdateHud(alerts);
@@ -57,11 +67,6 @@ public sealed class ClientAlertsSystem : AlertsSystem
UpdateHud(alerts);
}
private void ClientAlertsHandleState(Entity<AlertsComponent> alerts, ref AfterAutoHandleStateEvent args)
{
UpdateHud(alerts);
}
private void UpdateHud(Entity<AlertsComponent> entity)
{
if (_playerManager.LocalEntity == entity.Owner)

View File

@@ -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<IEntityManager>();
_spriteSystem = _entManager.System<SpriteSystem>();
_navMapSystem = _entManager.System<SharedNavMapSystem>();
// 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<NavMapRegionOverlay, int>();
if (_owner != null &&
_entManager.TryGetComponent<TransformComponent>(_owner, out var xform) &&
_entManager.TryGetComponent<NavMapComponent>(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;

View File

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

View File

@@ -0,0 +1,21 @@
using Content.Shared.Timing;
using Content.Shared.Cargo.Systems;
namespace Content.Client.Cargo.Systems;
/// <summary>
/// This handles...
/// </summary>
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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<DamageContainerPrototype>(arg)).ToList(),
HealthStatusIcon = null,
NetSyncEnabled = false
};

View File

@@ -124,6 +124,10 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
continue;
}
var targetEv = new GetFlashEffectTargetEvent(ent);
RaiseLocalEvent(ent, ref targetEv);
ent = targetEv.Target;
EnsureComp<ColorFlashEffectComponent>(ent, out comp);
comp.NetSyncEnabled = false;
comp.Color = sprite.Color;
@@ -132,3 +136,9 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
}
}
}
/// <summary>
/// Raised on an entity to change the target for a color flash effect.
/// </summary>
[ByRefEvent]
public record struct GetFlashEffectTargetEvent(EntityUid Target);

View File

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

View File

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

View File

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

View File

@@ -47,8 +47,7 @@
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Horizontal"
HorizontalExpand="True" HorizontalAlignment="Center">
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
</BoxContainer>

View File

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

View File

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

View File

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

View File

@@ -98,7 +98,7 @@ namespace Content.Client.Inventory
}
}
if (EntMan.TryGetComponent<HandsComponent>(Owner, out var handsComp))
if (EntMan.TryGetComponent<HandsComponent>(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...

View File

@@ -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<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<ISharedChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
@@ -47,10 +52,13 @@ namespace Content.Client.IoC
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
}
}
}

View File

@@ -8,7 +8,7 @@
VerticalExpand="False"
VerticalAlignment="Bottom"
HorizontalAlignment="Center">
<controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True"
<SplitContainer Name="ScreenContainer" HorizontalExpand="True"
VerticalExpand="True" SplitWidth="0"
StretchDirection="TopLeft">
<BoxContainer Orientation="Vertical" VerticalExpand="True" Name="SpawnContainer" MinWidth="200" SetWidth="600">
@@ -82,5 +82,5 @@
</BoxContainer>
</PanelContainer>
</LayoutContainer>
</controls:RecordedSplitContainer>
</SplitContainer>
</mapping:MappingScreen>

View File

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

View File

@@ -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<StartedWaddlingEvent>(OnStartWaddling);
SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeAllEvent<StoppedWaddlingEvent>(OnStopWaddling);
}
private void OnStartWaddling(StartedWaddlingEvent msg, EntitySessionEventArgs args)
{
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
StartWaddling((GetEntity(msg.Entity), comp));
}
private void OnStopWaddling(StoppedWaddlingEvent msg, EntitySessionEventArgs args)
{
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
StopWaddling((GetEntity(msg.Entity), comp));
}
private void StartWaddling(Entity<WaddleAnimationComponent> entity)
{
if (_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
return;
if (!TryComp<InputMoverComponent>(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<WaddleAnimationComponent> entity, ref AnimationCompletedEvent args)
{
if (args.Key != entity.Comp.KeyName)
return;
if (!TryComp<InputMoverComponent>(entity.Owner, out var mover))
return;
PlayWaddleAnimationUsing(
(entity.Owner, entity.Comp),
CalculateAnimationLength(entity.Comp, mover),
CalculateTumbleIntensity(entity.Comp)
);
}
private void StopWaddling(Entity<WaddleAnimationComponent> entity)
{
if (!_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
return;
_animation.Stop(entity.Owner, entity.Comp.KeyName);
if (!TryComp<SpriteComponent>(entity.Owner, out var sprite))
return;
sprite.Offset = new Vector2();
sprite.Rotation = Angle.FromDegrees(0);
}
private void PlayWaddleAnimationUsing(Entity<WaddleAnimationComponent> 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);
}
}

View File

@@ -22,6 +22,6 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsCo
return;
if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype!);
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -319,6 +319,8 @@ namespace Content.Client.Paper.UI
private void RunOnSaved()
{
// Prevent further saving while text processing still in
SaveButton.Disabled = true;
OnSaved?.Invoke(Rope.Collapse(Input.TextRope));
}

View File

@@ -1,9 +1,12 @@
using Content.Shared.Alert;
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -14,6 +17,8 @@ public sealed class MoverController : SharedMoverController
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override void Initialize()
{
@@ -135,4 +140,15 @@ public sealed class MoverController : SharedMoverController
{
return _timing is { IsFirstTimePredicted: true, InSimulation: true };
}
public override void SetSprinting(Entity<InputMoverComponent> 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);
}
}

View File

@@ -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<NavMapComponent>();
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<Vector2i>, HashSet<Vector2i>) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties)
{
if (!regionProperties.Seeds.Any())
return (new(), new());
var visitedChunks = new HashSet<Vector2i>();
var visitedTiles = new HashSet<Vector2i>();
var tilesToVisit = new Stack<Vector2i>();
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<Vector2i> 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;
}
}

View File

@@ -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<Vector2i, int[]> modifiedChunks;
Dictionary<NetEntity, NavMapBeacon> beacons;
Dictionary<NetEntity, NavMapRegionProperties> 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<NetEntity>();
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)
{

View File

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

View File

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

View File

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

View File

@@ -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<AppearanceComponent> _appearanceQuery;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ChameleonDisguisedComponent, GetFlashEffectTargetEvent>(OnGetFlashEffectTargetEvent);
}
private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
@@ -25,9 +34,30 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
CopyComp<SpriteComponent>(ent);
CopyComp<GenericVisualizerComponent>(ent);
CopyComp<SolutionContainerVisualsComponent>(ent);
CopyComp<BurnStateVisualsComponent>(ent);
// reload appearance to hopefully prevent any invisible layers
if (_appearanceQuery.TryComp(ent, out var appearance))
_appearance.QueueUpdate(ent, appearance);
}
private void OnStartup(Entity<ChameleonDisguisedComponent> ent, ref ComponentStartup args)
{
if (!_spriteQuery.TryComp(ent, out var sprite))
return;
ent.Comp.WasVisible = sprite.Visible;
sprite.Visible = false;
}
private void OnShutdown(Entity<ChameleonDisguisedComponent> ent, ref ComponentShutdown args)
{
if (_spriteQuery.TryComp(ent, out var sprite))
sprite.Visible = ent.Comp.WasVisible;
}
private void OnGetFlashEffectTargetEvent(Entity<ChameleonDisguisedComponent> ent, ref GetFlashEffectTargetEvent args)
{
args.Target = ent.Comp.Disguise;
}
}

View File

@@ -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<ApcMenu>();
_menu.SetEntity(Owner);
_menu.OnBreaker += BreakerPressed;
var hasAccess = false;
if (PlayerManager.LocalEntity != null)
{
var accessReader = EntMan.System<AccessReaderSystem>();
hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner);
}
_menu?.SetAccessEnabled(hasAccess);
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

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

View File

@@ -1,29 +0,0 @@
using System.Numerics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A split container that performs an action when the split resizing is finished.
/// </summary>
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);
}
}

View File

@@ -14,7 +14,7 @@
VerticalExpand="False"
VerticalAlignment="Bottom"
HorizontalAlignment="Center">
<controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True" VerticalExpand="True" SplitWidth="0" StretchDirection="TopLeft">
<SplitContainer Name="ScreenContainer" HorizontalExpand="True" VerticalExpand="True" SplitWidth="0" StretchDirection="TopLeft">
<LayoutContainer Name="ViewportContainer" HorizontalExpand="True" VerticalExpand="True">
<controls:MainViewport Name="MainViewport"/>
<widgets:GhostGui Name="Ghost" Access="Protected" />
@@ -26,7 +26,7 @@
</BoxContainer>
<alerts:AlertsUI Name="Alerts" Access="Protected" />
</LayoutContainer>
<PanelContainer HorizontalExpand="True" MinWidth="300">
<PanelContainer Name="SeparatedChatPanel" MinWidth="300">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2B2C3B" />
</PanelContainer.PanelOverride>
@@ -36,5 +36,5 @@
<chat:ChatBox VerticalExpand="True" HorizontalExpand="True" Name="Chat" Access="Protected" MinSize="0 0"/>
</BoxContainer>
</PanelContainer>
</controls:RecordedSplitContainer>
</SplitContainer>
</screens:SeparatedChatGameScreen>

View File

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

View File

@@ -398,10 +398,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
QueueWindowUpdate();
// TODO ACTIONS allow buttons to persist across state applications
// Then we don't have to interrupt drags any time the buttons get rebuilt.
_menuDragHelper.EndDrag();
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _actions.ToArray());
}

View File

@@ -28,14 +28,26 @@ public class ActionButtonContainer : GridContainer
get => (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)

View File

@@ -0,0 +1,9 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
Margin="8 0 8 0">
<BoxContainer Name="Buttons"
Orientation="Vertical"
SeparationOverride="5">
<!-- Buttons are added here by code -->
</BoxContainer>
</BoxContainer>

View File

@@ -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<GhostRoleInfo>? OnRoleSelected;
public event Action<GhostRoleInfo>? OnRoleFollow;
public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
public GhostRoleButtonsBox(bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
{
RobustXamlLoader.Load(this);
_spriteSystem = spriteSystem;
Title.Text = name;
Description.SetMessage(description);
foreach (var role in roles)
{
var button = new GhostRoleEntryButtons(role);

View File

@@ -1,15 +1,15 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Horizontal">
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Button Name="RequestButton"
Access="Public"
Text="{Loc 'ghost-roles-window-request-role-button'}"
StyleClasses="OpenRight"
HorizontalAlignment="Left"
SetWidth="300"/>
HorizontalExpand="True"
SizeFlagsStretchRatio="3"/>
<Button Name="FollowButton"
Access="Public"
Text="{Loc 'ghost-roles-window-follow-role-button'}"
StyleClasses="OpenLeft"
HorizontalAlignment="Right"
SetWidth="150"/>
HorizontalExpand="True"/>
</BoxContainer>

View File

@@ -0,0 +1,8 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical">
<Label Name="Title"
StyleClasses="LabelKeyText"/>
<PanelContainer StyleClasses="HighDivider" />
<RichTextLabel Name="Description"
Margin="0 4"/>
</BoxContainer>

View File

@@ -0,0 +1,18 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
[GenerateTypedNameReferences]
public sealed partial class GhostRoleInfoBox : BoxContainer
{
public GhostRoleInfoBox(string name, string description)
{
RobustXamlLoader.Load(this);
Title.Text = name;
Description.SetMessage(description);
}
}
}

View File

@@ -1,16 +0,0 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0 0 8 8">
<Label Name="Title"
StyleClasses="LabelKeyText"/>
<PanelContainer StyleClasses="HighDivider" />
<RichTextLabel Name="Description"
Margin="0 4"/>
<BoxContainer Name="Buttons"
HorizontalAlignment="Left"
Orientation="Vertical"
SeparationOverride="5">
<!-- Buttons are added here by code -->
</BoxContainer>
</BoxContainer>

View File

@@ -5,7 +5,6 @@ using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
@@ -77,6 +76,13 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
if (state is not GhostRolesEuiState ghostState)
return;
// We must save BodyVisible state, so all Collapsible boxes will not close
// on adding new ghost role.
// Save the current state of each Collapsible box being visible or not
_window.SaveCollapsibleBoxesStates();
// Clearing the container before adding new roles
_window.ClearEntries();
var entityManager = IoCManager.Resolve<IEntityManager>();
@@ -84,28 +90,32 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
// Grouping roles
var groupedRoles = ghostState.GhostRoles.GroupBy(
role => (role.Name, role.Description, role.Requirements));
// Add a new entry for each role group
foreach (var group in groupedRoles)
{
var name = group.Key.Name;
var description = group.Key.Description;
bool hasAccess = true;
FormattedMessage? reason;
if (!requirementsManager.CheckRoleRequirements(group.Key.Requirements, null, out reason))
{
hasAccess = false;
}
var hasAccess = requirementsManager.CheckRoleRequirements(
group.Key.Requirements,
null,
out var reason);
// Adding a new role
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
}
// Restore the Collapsible box state if it is saved
_window.RestoreCollapsibleBoxesStates();
// Close the rules window if it is no longer needed
var closeRulesWindow = ghostState.GhostRoles.All(role => role.Identifier != _windowRulesId);
if (closeRulesWindow)
{
_windowRules?.Close();
}
}
}
}

View File

@@ -1,7 +1,10 @@
using System.Linq;
using Content.Shared.Ghost.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
@@ -12,20 +15,86 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
public event Action<GhostRoleInfo>? OnRoleRequestButtonClicked;
public event Action<GhostRoleInfo>? OnRoleFollow;
private Dictionary<(string name, string description), Collapsible> _collapsibleBoxes = new();
private HashSet<(string name, string description)> _uncollapsedStates = new();
public GhostRolesWindow()
{
RobustXamlLoader.Load(this);
}
public void ClearEntries()
{
NoRolesMessage.Visible = true;
EntryContainer.DisposeAllChildren();
_collapsibleBoxes.Clear();
}
public void SaveCollapsibleBoxesStates()
{
_uncollapsedStates.Clear();
foreach (var (key, collapsible) in _collapsibleBoxes)
{
if (collapsible.BodyVisible)
{
_uncollapsedStates.Add(key);
}
}
}
public void RestoreCollapsibleBoxesStates()
{
foreach (var (key, collapsible) in _collapsibleBoxes)
{
collapsible.BodyVisible = _uncollapsedStates.Contains(key);
}
}
public void AddEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
{
NoRolesMessage.Visible = false;
var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
entry.OnRoleSelected += OnRoleRequestButtonClicked;
entry.OnRoleFollow += OnRoleFollow;
EntryContainer.AddChild(entry);
var ghostRoleInfos = roles.ToList();
var rolesCount = ghostRoleInfos.Count;
var info = new GhostRoleInfoBox(name, description);
var buttons = new GhostRoleButtonsBox(hasAccess, reason, ghostRoleInfos, spriteSystem);
buttons.OnRoleSelected += OnRoleRequestButtonClicked;
buttons.OnRoleFollow += OnRoleFollow;
EntryContainer.AddChild(info);
if (rolesCount > 1)
{
var buttonHeading = new CollapsibleHeading(Loc.GetString("ghost-roles-window-available-button", ("rolesCount", rolesCount)));
buttonHeading.AddStyleClass(ContainerButton.StyleClassButton);
buttonHeading.Label.HorizontalAlignment = HAlignment.Center;
buttonHeading.Label.HorizontalExpand = true;
var body = new CollapsibleBody
{
Margin = new Thickness(0, 5, 0, 0),
};
// TODO: Add Requirements to this key when it'll be fixed and work as an equality key in GhostRolesEui
var key = (name, description);
var collapsible = new Collapsible(buttonHeading, body)
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(0, 0, 0, 8),
};
body.AddChild(buttons);
EntryContainer.AddChild(collapsible);
_collapsibleBoxes.Add(key, collapsible);
}
else
{
EntryContainer.AddChild(buttons);
}
}
}
}

View File

@@ -2,7 +2,8 @@
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
MinHeight="210">
<BoxContainer Name="MainContainer" Orientation="Vertical">
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
using Content.Client.Gameplay;
using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Content.Shared.CCVar;
@@ -8,6 +9,7 @@ using Content.Shared.Voting;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.State;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -28,6 +30,7 @@ namespace Content.Client.Voting.UI
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _entNetManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IStateManager _state = default!;
private VotingSystem _votingSystem;
@@ -62,7 +65,7 @@ namespace Content.Client.Voting.UI
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
CloseButton.OnPressed += _ => Close();
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime) / 60));
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime)));
foreach (StandardVoteType voteType in Enum.GetValues<StandardVoteType>())
{
@@ -70,6 +73,7 @@ namespace Content.Client.Voting.UI
VoteTypeButton.AddItem(Loc.GetString(option.Name), (int)voteType);
}
_state.OnStateChanged += OnStateChanged;
VoteTypeButton.OnItemSelected += VoteTypeSelected;
CreateButton.OnPressed += CreatePressed;
FollowButton.OnPressed += FollowSelected;
@@ -101,6 +105,14 @@ namespace Content.Client.Voting.UI
UpdateVoteTimeout();
}
private void OnStateChanged(StateChangedEventArgs obj)
{
if (obj.NewState is not GameplayState)
return;
Close();
}
private void CanCallVoteChanged(bool obj)
{
if (!obj)

View File

@@ -19,11 +19,6 @@ public sealed class VotingSystem : EntitySystem
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
{
if (!_ghostSystem.IsGhost)
{
return;
}
VotePlayerListResponse?.Invoke(msg);
}

View File

@@ -28,6 +28,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly MapSystem _map = default!;
private EntityQuery<TransformComponent> _xformQuery;
@@ -109,11 +110,11 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
{
coordinates = EntityCoordinates.FromMap(gridUid, mousePos, TransformSystem, EntityManager);
coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
}
else
{
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, TransformSystem, EntityManager);
coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
}
// Heavy attack.

View File

@@ -157,7 +157,7 @@ public sealed partial class GunSystem : SharedGunSystem
var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down)
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
{
if (gun.ShotCounter != 0)
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });

View File

@@ -35,9 +35,9 @@ public sealed partial class TestPair
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
mapData.Grid.Comp.SetTile(mapData.GridCoords, platingTile);
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapData.Grid.Comp.GetAllTiles().First();
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
TestMap = mapData;
@@ -107,13 +107,41 @@ public sealed partial class TestPair
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<EntityPrototype> GetPrototypesWithComponent<T>(
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
where T : IComponent
{
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(T));
var list = new List<(EntityPrototype, T)>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
if (ignored != null && ignored.Contains(proto.ID))
continue;
if (ignoreAbstract && proto.Abstract)
continue;
if (ignoreTestPrototypes && IsTestPrototype(proto))
continue;
if (proto.Components.TryGetComponent(id, out var cmp))
list.Add((proto, (T)cmp));
}
return list;
}
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
{
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
var list = new List<EntityPrototype>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
@@ -127,7 +155,7 @@ public sealed partial class TestPair
continue;
if (proto.Components.ContainsKey(id))
list.Add(proto);
list.Add((proto));
}
return list;

View File

@@ -36,7 +36,9 @@ public static partial class PoolManager
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0")
(CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
};
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)

View File

@@ -168,7 +168,7 @@ public sealed class SuicideCommandTests
await pair.CleanReturnAsync();
}
/// <summary>
/// <summary>
/// Run the suicide command in the console
/// Should only ghost the player but not kill them
/// </summary>
@@ -241,6 +241,7 @@ public sealed class SuicideCommandTests
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
var transformSystem = entManager.System<TransformSystem>();
var damageableSystem = entManager.System<DamageableSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
@@ -276,6 +277,8 @@ public sealed class SuicideCommandTests
// and that all the damage is concentrated in the Slash category
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
damageableSystem.SetAllDamage(player, damageableComp, 0);
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
@@ -313,6 +316,7 @@ public sealed class SuicideCommandTests
var mindSystem = entManager.System<SharedMindSystem>();
var mobStateSystem = entManager.System<MobStateSystem>();
var transformSystem = entManager.System<TransformSystem>();
var damageableSystem = entManager.System<DamageableSystem>();
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
var player = playerMan.Sessions.First().AttachedEntity!.Value;
@@ -348,6 +352,8 @@ public sealed class SuicideCommandTests
// and that slash damage is split in half
await server.WaitAssertion(() =>
{
// Heal all damage first (possible low pressure damage taken)
damageableSystem.SetAllDamage(player, damageableComp, 0);
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();

View File

@@ -1,4 +1,5 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Body.Components;
using Content.Server.GameTicking;
@@ -120,8 +121,8 @@ public sealed class NukeOpsTest
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
var roles = roleSys.MindGetAllRoles(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
var roles = roleSys.MindGetAllRoleInfo(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The second dummy player should be a medic
@@ -131,8 +132,8 @@ public sealed class NukeOpsTest
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
roles = roleSys.MindGetAllRoles(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic" && x.Component is NukeopsRoleComponent);
roles = roleSys.MindGetAllRoleInfo(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
// The other two players should have just spawned in as normal.
@@ -141,13 +142,14 @@ public sealed class NukeOpsTest
void CheckDummy(int i)
{
var ent = dummyEnts[i];
var mind = mindSys.GetMind(ent)!.Value;
var mindCrew = mindSys.GetMind(ent)!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mind), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind), Is.False);
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
Assert.That(roleSys.MindGetAllRoles(mind).Any(x => x.Component is NukeopsRoleComponent), Is.False);
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}
// The game rule exists, and all the stations/shuttles/maps are properly initialized
@@ -238,7 +240,8 @@ public sealed class NukeOpsTest
for (var i = 0; i < nukies.Length - 1; i++)
{
entMan.DeleteEntity(nukies[i]);
Assert.That(roundEndSys.IsRoundEndRequested, Is.False,
Assert.That(roundEndSys.IsRoundEndRequested,
Is.False,
$"The round ended, but {nukies.Length - i - 1} nukies are still alive!");
}
// Delete the last nukie and make sure the round ends.

View File

@@ -2,7 +2,6 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
using Content.Shared.Roles.Jobs;
namespace Content.IntegrationTests.Tests.Internals;
@@ -25,10 +24,7 @@ public sealed class AutoInternalsTests
await server.WaitAssertion(() =>
{
var profile = new HumanoidCharacterProfile();
var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, new JobComponent()
{
Prototype = "TestInternalsDummy"
}, profile, station: null);
var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, "TestInternalsDummy", profile, station: null);
Assert.That(atmos.HasAtmosphere(testMap.Grid), Is.False, "Test map has atmosphere - test needs adjustment!");
Assert.That(internals.AreInternalsWorking(dummy), "Internals did not automatically connect!");

View File

@@ -1,10 +1,8 @@
#nullable enable
using System.Linq;
using Content.Server.Ghost;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind.Commands;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
@@ -18,7 +16,6 @@ using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -287,27 +284,27 @@ public sealed partial class MindTests
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
var traitorRole = new TraitorRoleComponent();
var traitorRole = "MindRoleTraitor";
roleSystem.MindAddRole(mindId, traitorRole);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
var jobRole = new JobComponent();
var jobRole = "";
roleSystem.MindAddRole(mindId, jobRole);
roleSystem.MindAddJobRole(mindId, jobPrototype:jobRole);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
});
roleSystem.MindRemoveRole<TraitorRoleComponent>(mindId);
@@ -315,15 +312,15 @@ public sealed partial class MindTests
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
});
roleSystem.MindRemoveRole<JobComponent>(mindId);
roleSystem.MindRemoveRole<JobRoleComponent>(mindId);
Assert.Multiple(() =>
{
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
});
});

View File

@@ -0,0 +1,95 @@
using System.Linq;
using Content.Server.Roles;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;
namespace Content.IntegrationTests.Tests.Minds;
[TestFixture]
public sealed class RoleTests
{
/// <summary>
/// Check that any prototype with a <see cref="MindRoleComponent"/> is properly configured
/// </summary>
[Test]
public async Task ValidateRolePrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var jobComp = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(JobRoleComponent));
Assert.Multiple(() =>
{
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<MindRoleComponent>())
{
Assert.That(comp.AntagPrototype == null || comp.JobPrototype == null, $"Role {proto.ID} has both a job and antag prototype.");
Assert.That(!comp.ExclusiveAntag || comp.Antag, $"Role {proto.ID} is marked as an exclusive antag, despite not being an antag.");
Assert.That(comp.Antag || comp.AntagPrototype == null, $"Role {proto.ID} has an antag prototype, despite not being an antag.");
if (comp.JobPrototype != null)
Assert.That(proto.Components.ContainsKey(jobComp), $"Role {proto.ID} is a job, despite not having a job prototype.");
// It is possible that this is meant to be supported? Though I would assume that it would be for
// admin / prototype uploads, and that pre-defined roles should still check this.
Assert.That(!comp.Antag || comp.AntagPrototype != null , $"Role {proto.ID} is an antag, despite not having a antag prototype.");
}
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that any prototype with a <see cref="JobRoleComponent"/> also has a properly configured
/// <see cref="MindRoleComponent"/>
/// </summary>
[Test]
public async Task ValidateJobPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
Assert.Multiple(() =>
{
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<JobRoleComponent>())
{
if (proto.Components.TryGetComponent(mindCompId, out var mindComp))
Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null);
}
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that any prototype with a component that inherits from <see cref="BaseMindRoleComponent"/> also has a
/// <see cref="MindRoleComponent"/>
/// </summary>
[Test]
public async Task ValidateRolesHaveMindRoleComp()
{
await using var pair = await PoolManager.GetServerClient();
var refMan = pair.Server.ResolveDependency<IReflectionManager>();
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
var compTypes = refMan.GetAllChildren(typeof(BaseMindRoleComponent))
.Append(typeof(RoleBriefingComponent))
.Where(x => !x.IsAbstract);
Assert.Multiple(() =>
{
foreach (var comp in compTypes)
{
foreach (var proto in pair.GetPrototypesWithComponent(comp))
{
Assert.That(proto.Components.ContainsKey(mindCompId), $"Role {proto.ID} does not have a {nameof(MindRoleComponent)} despite having a {comp.Name}");
}
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.Station.Systems;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
@@ -68,10 +67,7 @@ public sealed class LoadoutTests
profile.SetLoadout(new RoleLoadout("LoadoutTester"));
var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
{
Prototype = "LoadoutTester"
}, profile, station: null);
var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: "LoadoutTester", profile, station: null);
var slotQuery = inventorySystem.GetSlotEnumerator(tester);
var checkedCount = 0;

View File

@@ -35,15 +35,16 @@ public sealed class StartingGearPrototypeStorageTest
{
foreach (var gearProto in protos)
{
var backpackProto = ((IEquipmentLoadout) gearProto).GetGear("back");
if (backpackProto == string.Empty)
continue;
var bag = server.EntMan.SpawnEntity(backpackProto, coords);
var ents = new ValueList<EntityUid>();
foreach (var (slot, entProtos) in gearProto.Storage)
{
ents.Clear();
var storageProto = ((IEquipmentLoadout)gearProto).GetGear(slot);
if (storageProto == string.Empty)
continue;
var bag = server.EntMan.SpawnEntity(storageProto, coords);
if (entProtos.Count == 0)
continue;
@@ -59,9 +60,8 @@ public sealed class StartingGearPrototypeStorageTest
server.EntMan.DeleteEntity(ent);
}
server.EntMan.DeleteEntity(bag);
}
server.EntMan.DeleteEntity(bag);
}
mapManager.DeleteMap(testMap.MapId);

View File

@@ -40,7 +40,7 @@ public sealed class PrototypeSaveTest
await pair.Client.WaitPost(() =>
{
foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
foreach (var (proto, _) in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
{
var dummy = pair.Client.EntMan.Spawn(proto.ID);
pair.Client.EntMan.RunMapInit(dummy, pair.Client.MetaData(dummy));

View File

@@ -94,14 +94,13 @@ namespace Content.IntegrationTests.Tests
await Assert.MultipleAsync(async () =>
{
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
StorageComponent? storage = null;
ItemComponent? item = null;
StorageFillComponent fill = default!;
var size = 0;
await server.WaitAssertion(() =>
{
@@ -112,7 +111,6 @@ namespace Content.IntegrationTests.Tests
}
proto.TryGetComponent("Item", out item);
fill = (StorageFillComponent) proto.Components[id].Component;
size = GetFillSize(fill, false, protoMan, itemSys);
});
@@ -179,7 +177,7 @@ namespace Content.IntegrationTests.Tests
var itemSys = entMan.System<SharedItemSystem>();
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
@@ -192,7 +190,6 @@ namespace Content.IntegrationTests.Tests
if (entStorage == null)
return;
var fill = (StorageFillComponent) proto.Components[id].Component;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");

View File

@@ -67,7 +67,7 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon);
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon);
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
}

View File

@@ -96,7 +96,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
PrivilegedIdIsAuthorized(uid, component),
true,
targetIdComponent.FullName,
targetIdComponent.JobTitle,
targetIdComponent.LocalizedJobTitle,
targetAccessComponent.Tags.ToList(),
possibleAccess,
jobProto,

View File

@@ -14,13 +14,13 @@ using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Managers;
@@ -45,14 +45,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public const string SawmillId = "admin.bans";
public const string JobPrefix = "Job:";
private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new();
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
// Cached ban exemption flags are used to handle
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
public void Initialize()
{
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToNotifications(OnDatabaseNotification);
@@ -63,12 +61,23 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
{
// Yeah so role ban loading code isn't integrated with exempt flag loading code.
// Have you seen how garbage role ban code code is? I don't feel like refactoring it right now.
var flags = await _db.GetBanExemption(player.UserId, cancel);
var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
cancel.ThrowIfCancellationRequested();
_cachedBanExemptions[player] = flags;
_cachedRoleBans[player] = userRoleBans;
SendRoleBans(player);
}
private void ClearPlayerData(ICommonSession player)
@@ -76,25 +85,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
_cachedBanExemptions.Remove(player);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId))
return;
var netChannel = e.Session.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, hwId);
SendRoleBans(e.Session);
}
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
banDef = await _db.AddServerRoleBanAsync(banDef);
if (banDef.UserId != null)
if (banDef.UserId != null
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
_cachedRoleBans.GetOrNew(banDef.UserId.Value).Add(banDef);
cachedBans.Add(banDef);
}
return true;
@@ -102,31 +101,21 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
}
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
{
var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false);
var userRoleBans = new HashSet<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
_cachedRoleBans[userId] = userRoleBans;
}
public void Restart()
{
// Clear out players that have disconnected.
var toRemove = new List<NetUserId>();
var toRemove = new ValueList<ICommonSession>();
foreach (var player in _cachedRoleBans.Keys)
{
if (!_playerManager.TryGetSessionById(player, out _))
if (player.Status == SessionStatus.Disconnected)
toRemove.Add(player);
}
@@ -138,7 +127,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
// Check for expired bans
foreach (var roleBans in _cachedRoleBans.Values)
{
roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime);
roleBans.RemoveAll(ban => DateTimeOffset.Now > ban.ExpirationTime);
}
}
@@ -281,9 +270,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
if (target != null)
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
{
SendRoleBans(target.Value);
SendRoleBans(session);
}
}
@@ -311,10 +300,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
if (ban.UserId is { } player && _cachedRoleBans.TryGetValue(player, out var roleBans))
if (ban.UserId is { } player
&& _playerManager.TryGetSessionById(player, out var session)
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
{
roleBans.RemoveWhere(roleBan => roleBan.Id == ban.Id);
SendRoleBans(player);
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
SendRoleBans(session);
}
return $"Pardoned ban with id {banId}";
@@ -322,8 +313,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
if (!_cachedRoleBans.TryGetValue(session, out var roleBans))
return null;
return roleBans
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
@@ -331,19 +326,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#endregion
public void SendRoleBans(NetUserId userId)
{
if (!_playerManager.TryGetSessionById(userId, out var player))
{
return;
}
SendRoleBans(player);
}
public void SendRoleBans(ICommonSession pSession)
{
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession.UserId) ?? new HashSet<ServerRoleBanDef>();
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>();
var bans = new MsgRoleBans()
{
Bans = roleBans.Select(o => o.Role).ToList()

View File

@@ -47,12 +47,6 @@ public interface IBanManager
/// <param name="unbanTime">The time at which this role ban was pardoned.</param>
public Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime);
/// <summary>
/// Sends role bans to the target
/// </summary>
/// <param name="pSession">Player's user ID</param>
public void SendRoleBans(NetUserId userId);
/// <summary>
/// Sends role bans to the target
/// </summary>

View File

@@ -95,9 +95,10 @@ public sealed partial class AdminVerbSystem
if (HasComp<MapComponent>(args.Target) || HasComp<MapGridComponent>(args.Target))
return;
var explodeName = Loc.GetString("admin-smite-explode-name").ToLowerInvariant();
Verb explode = new()
{
Text = "admin-smite-explode-name",
Text = explodeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
Act = () =>
@@ -111,13 +112,14 @@ public sealed partial class AdminVerbSystem
_bodySystem.GibBody(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-explode-description")
Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console.
};
args.Verbs.Add(explode);
var chessName = Loc.GetString("admin-smite-chess-dimension-name").ToLowerInvariant();
Verb chess = new()
{
Text = "admin-smite-chess-dimension-name",
Text = chessName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
Act = () =>
@@ -137,12 +139,13 @@ public sealed partial class AdminVerbSystem
xform.WorldRotation = Angle.Zero;
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-chess-dimension-description")
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))
};
args.Verbs.Add(chess);
if (TryComp<FlammableComponent>(args.Target, out var flammable))
{
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
Verb flames = new()
{
Text = "admin-smite-set-alight-name",
@@ -160,14 +163,15 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-set-alight-description")
Message = string.Join(": ", flamesName, Loc.GetString("admin-smite-set-alight-description"))
};
args.Verbs.Add(flames);
}
var monkeyName = Loc.GetString("admin-smite-monkeyify-name").ToLowerInvariant();
Verb monkey = new()
{
Text = "admin-smite-monkeyify-name",
Text = monkeyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
Act = () =>
@@ -175,13 +179,14 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-monkeyify-description")
Message = string.Join(": ", monkeyName, Loc.GetString("admin-smite-monkeyify-description"))
};
args.Verbs.Add(monkey);
var disposalBinName = Loc.GetString("admin-smite-garbage-can-name").ToLowerInvariant();
Verb disposalBin = new()
{
Text = "admin-smite-electrocute-name",
Text = disposalBinName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
Act = () =>
@@ -189,16 +194,17 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-garbage-can-description")
Message = string.Join(": ", disposalBinName, Loc.GetString("admin-smite-garbage-can-description"))
};
args.Verbs.Add(disposalBin);
if (TryComp<DamageableComponent>(args.Target, out var damageable) &&
HasComp<MobStateComponent>(args.Target))
{
var hardElectrocuteName = Loc.GetString("admin-smite-electrocute-name").ToLowerInvariant();
Verb hardElectrocute = new()
{
Text = "admin-smite-creampie-name",
Text = hardElectrocuteName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
Act = () =>
@@ -234,16 +240,17 @@ public sealed partial class AdminVerbSystem
TimeSpan.FromSeconds(30), refresh: true, ignoreInsulation: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-electrocute-description")
Message = string.Join(": ", hardElectrocuteName, Loc.GetString("admin-smite-electrocute-description"))
};
args.Verbs.Add(hardElectrocute);
}
if (TryComp<CreamPiedComponent>(args.Target, out var creamPied))
{
var creamPieName = Loc.GetString("admin-smite-creampie-name").ToLowerInvariant();
Verb creamPie = new()
{
Text = "admin-smite-remove-blood-name",
Text = creamPieName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
Act = () =>
@@ -251,16 +258,17 @@ public sealed partial class AdminVerbSystem
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-creampie-description")
Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description"))
};
args.Verbs.Add(creamPie);
}
if (TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
{
var bloodRemovalName = Loc.GetString("admin-smite-remove-blood-name").ToLowerInvariant();
Verb bloodRemoval = new()
{
Text = "admin-smite-vomit-organs-name",
Text = bloodRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
Act = () =>
@@ -273,7 +281,7 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-blood-description")
Message = string.Join(": ", bloodRemovalName, Loc.GetString("admin-smite-remove-blood-description"))
};
args.Verbs.Add(bloodRemoval);
}
@@ -281,9 +289,10 @@ public sealed partial class AdminVerbSystem
// bobby...
if (TryComp<BodyComponent>(args.Target, out var body))
{
var vomitOrgansName = Loc.GetString("admin-smite-vomit-organs-name").ToLowerInvariant();
Verb vomitOrgans = new()
{
Text = "admin-smite-remove-hands-name",
Text = vomitOrgansName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/vomit_toxin.rsi"), "vomit_toxin-1"),
Act = () =>
@@ -305,13 +314,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-vomit-organs-description")
Message = string.Join(": ", vomitOrgansName, Loc.GetString("admin-smite-vomit-organs-description"))
};
args.Verbs.Add(vomitOrgans);
var handsRemovalName = Loc.GetString("admin-smite-remove-hands-name").ToLowerInvariant();
Verb handsRemoval = new()
{
Text = "admin-smite-remove-hand-name",
Text = handsRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
Act = () =>
@@ -327,13 +337,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.Medium);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-hands-description")
Message = string.Join(": ", handsRemovalName, Loc.GetString("admin-smite-remove-hands-description"))
};
args.Verbs.Add(handsRemoval);
var handRemovalName = Loc.GetString("admin-smite-remove-hand-name").ToLowerInvariant();
Verb handRemoval = new()
{
Text = "admin-smite-pinball-name",
Text = handRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
Act = () =>
@@ -350,13 +361,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.Medium);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-hand-description")
Message = string.Join(": ", handRemovalName, Loc.GetString("admin-smite-remove-hand-description"))
};
args.Verbs.Add(handRemoval);
var stomachRemovalName = Loc.GetString("admin-smite-stomach-removal-name").ToLowerInvariant();
Verb stomachRemoval = new()
{
Text = "admin-smite-yeet-name",
Text = stomachRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
Act = () =>
@@ -370,13 +382,14 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-stomach-removal-description"),
Message = string.Join(": ", stomachRemovalName, Loc.GetString("admin-smite-stomach-removal-description"))
};
args.Verbs.Add(stomachRemoval);
var lungRemovalName = Loc.GetString("admin-smite-lung-removal-name").ToLowerInvariant();
Verb lungRemoval = new()
{
Text = "admin-smite-become-bread-name",
Text = lungRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
Act = () =>
@@ -390,16 +403,17 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-lung-removal-description"),
Message = string.Join(": ", lungRemovalName, Loc.GetString("admin-smite-lung-removal-description"))
};
args.Verbs.Add(lungRemoval);
}
if (TryComp<PhysicsComponent>(args.Target, out var physics))
{
var pinballName = Loc.GetString("admin-smite-pinball-name").ToLowerInvariant();
Verb pinball = new()
{
Text = "admin-smite-ghostkick-name",
Text = pinballName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
Act = () =>
@@ -427,13 +441,14 @@ public sealed partial class AdminVerbSystem
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-pinball-description")
Message = string.Join(": ", pinballName, Loc.GetString("admin-smite-pinball-description"))
};
args.Verbs.Add(pinball);
var yeetName = Loc.GetString("admin-smite-yeet-name").ToLowerInvariant();
Verb yeet = new()
{
Text = "admin-smite-nyanify-name",
Text = yeetName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Act = () =>
@@ -457,11 +472,12 @@ public sealed partial class AdminVerbSystem
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-yeet-description")
Message = string.Join(": ", yeetName, Loc.GetString("admin-smite-yeet-description"))
};
args.Verbs.Add(yeet);
}
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
Verb bread = new()
{
Text = "admin-smite-kill-sign-name",
@@ -472,10 +488,11 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-bread-description")
Message = string.Join(": ", breadName, Loc.GetString("admin-smite-become-bread-description"))
};
args.Verbs.Add(bread);
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
Verb mouse = new()
{
Text = "admin-smite-cluwne-name",
@@ -486,15 +503,16 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-mouse-description")
Message = string.Join(": ", mouseName, Loc.GetString("admin-smite-become-mouse-description"))
};
args.Verbs.Add(mouse);
if (TryComp<ActorComponent>(args.Target, out var actorComponent))
{
var ghostKickName = Loc.GetString("admin-smite-ghostkick-name").ToLowerInvariant();
Verb ghostKick = new()
{
Text = "admin-smite-anger-pointing-arrows-name",
Text = ghostKickName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
Act = () =>
@@ -502,15 +520,18 @@ public sealed partial class AdminVerbSystem
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-ghostkick-description")
Message = string.Join(": ", ghostKickName, Loc.GetString("admin-smite-ghostkick-description"))
};
args.Verbs.Add(ghostKick);
}
if (TryComp<InventoryComponent>(args.Target, out var inventory)) {
if (TryComp<InventoryComponent>(args.Target, out var inventory))
{
var nyanifyName = Loc.GetString("admin-smite-nyanify-name").ToLowerInvariant();
Verb nyanify = new()
{
Text = "admin-smite-dust-name",
Text = nyanifyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
Act = () =>
@@ -521,13 +542,14 @@ public sealed partial class AdminVerbSystem
_inventorySystem.TryEquip(args.Target, ears, "head", true, true, false, inventory);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-nyanify-description")
Message = string.Join(": ", nyanifyName, Loc.GetString("admin-smite-nyanify-description"))
};
args.Verbs.Add(nyanify);
var killSignName = Loc.GetString("admin-smite-kill-sign-name").ToLowerInvariant();
Verb killSign = new()
{
Text = "admin-smite-buffering-name",
Text = killSignName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
Act = () =>
@@ -535,13 +557,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<KillSignComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-kill-sign-description")
Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description"))
};
args.Verbs.Add(killSign);
var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant();
Verb cluwne = new()
{
Text = "admin-smite-become-instrument-name",
Text = cluwneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
@@ -551,13 +574,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<CluwneComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-cluwne-description")
Message = string.Join(": ", cluwneName, Loc.GetString("admin-smite-cluwne-description"))
};
args.Verbs.Add(cluwne);
var maidenName = Loc.GetString("admin-smite-maid-name").ToLowerInvariant();
Verb maiden = new()
{
Text = "admin-smite-remove-gravity-name",
Text = maidenName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
Act = () =>
@@ -570,14 +594,15 @@ public sealed partial class AdminVerbSystem
});
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-maid-description")
Message = string.Join(": ", maidenName, Loc.GetString("admin-smite-maid-description"))
};
args.Verbs.Add(maiden);
}
var angerPointingArrowsName = Loc.GetString("admin-smite-anger-pointing-arrows-name").ToLowerInvariant();
Verb angerPointingArrows = new()
{
Text = "admin-smite-reptilian-species-swap-name",
Text = angerPointingArrowsName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
Act = () =>
@@ -585,13 +610,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<PointingArrowAngeringComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-anger-pointing-arrows-description")
Message = string.Join(": ", angerPointingArrowsName, Loc.GetString("admin-smite-anger-pointing-arrows-description"))
};
args.Verbs.Add(angerPointingArrows);
var dustName = Loc.GetString("admin-smite-dust-name").ToLowerInvariant();
Verb dust = new()
{
Text = "admin-smite-locker-stuff-name",
Text = dustName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
Act = () =>
@@ -601,13 +627,14 @@ public sealed partial class AdminVerbSystem
_popupSystem.PopupEntity(Loc.GetString("admin-smite-turned-ash-other", ("name", args.Target)), args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-dust-description"),
Message = string.Join(": ", dustName, Loc.GetString("admin-smite-dust-description"))
};
args.Verbs.Add(dust);
var youtubeVideoSimulationName = Loc.GetString("admin-smite-buffering-name").ToLowerInvariant();
Verb youtubeVideoSimulation = new()
{
Text = "admin-smite-headstand-name",
Text = youtubeVideoSimulationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
Act = () =>
@@ -615,27 +642,29 @@ public sealed partial class AdminVerbSystem
EnsureComp<BufferingComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-buffering-description"),
Message = string.Join(": ", youtubeVideoSimulationName, Loc.GetString("admin-smite-buffering-description"))
};
args.Verbs.Add(youtubeVideoSimulation);
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
Verb instrumentation = new()
{
Text = "admin-smite-become-mouse-name",
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-instrument-description"),
Message = string.Join(": ", instrumentationName, Loc.GetString("admin-smite-become-instrument-description"))
};
args.Verbs.Add(instrumentation);
var noGravityName = Loc.GetString("admin-smite-remove-gravity-name").ToLowerInvariant();
Verb noGravity = new()
{
Text = "admin-smite-maid-name",
Text = noGravityName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
Act = () =>
@@ -646,13 +675,14 @@ public sealed partial class AdminVerbSystem
Dirty(args.Target, grav);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-gravity-description"),
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
};
args.Verbs.Add(noGravity);
var reptilianName = Loc.GetString("admin-smite-reptilian-species-swap-name").ToLowerInvariant();
Verb reptilian = new()
{
Text = "admin-smite-zoom-in-name",
Text = reptilianName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
Act = () =>
@@ -660,13 +690,14 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-reptilian-species-swap-description"),
Message = string.Join(": ", reptilianName, Loc.GetString("admin-smite-reptilian-species-swap-description"))
};
args.Verbs.Add(reptilian);
var lockerName = Loc.GetString("admin-smite-locker-stuff-name").ToLowerInvariant();
Verb locker = new()
{
Text = "admin-smite-flip-eye-name",
Text = lockerName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
Act = () =>
@@ -682,10 +713,11 @@ public sealed partial class AdminVerbSystem
_weldableSystem.SetWeldedState(locker, true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-locker-stuff-description"),
Message = string.Join(": ", lockerName, Loc.GetString("admin-smite-locker-stuff-description"))
};
args.Verbs.Add(locker);
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
Verb headstand = new()
{
Text = "admin-smite-run-walk-swap-name",
@@ -696,13 +728,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<HeadstandComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-headstand-description"),
Message = string.Join(": ", headstandName, Loc.GetString("admin-smite-headstand-description"))
};
args.Verbs.Add(headstand);
var zoomInName = Loc.GetString("admin-smite-zoom-in-name").ToLowerInvariant();
Verb zoomIn = new()
{
Text = "admin-smite-super-speed-name",
Text = zoomInName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
Act = () =>
@@ -711,13 +744,14 @@ public sealed partial class AdminVerbSystem
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * 0.2f, ignoreLimits: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-zoom-in-description"),
Message = string.Join(": ", zoomInName, Loc.GetString("admin-smite-zoom-in-description"))
};
args.Verbs.Add(zoomIn);
var flipEyeName = Loc.GetString("admin-smite-flip-eye-name").ToLowerInvariant();
Verb flipEye = new()
{
Text = "admin-smite-stomach-removal-name",
Text = flipEyeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
Act = () =>
@@ -726,13 +760,14 @@ public sealed partial class AdminVerbSystem
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * -1, ignoreLimits: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-flip-eye-description"),
Message = string.Join(": ", flipEyeName, Loc.GetString("admin-smite-flip-eye-description"))
};
args.Verbs.Add(flipEye);
var runWalkSwapName = Loc.GetString("admin-smite-run-walk-swap-name").ToLowerInvariant();
Verb runWalkSwap = new()
{
Text = "admin-smite-speak-backwards-name",
Text = runWalkSwapName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
Act = () =>
@@ -746,13 +781,14 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-run-walk-swap-description"),
Message = string.Join(": ", runWalkSwapName, Loc.GetString("admin-smite-run-walk-swap-description"))
};
args.Verbs.Add(runWalkSwap);
var backwardsAccentName = Loc.GetString("admin-smite-speak-backwards-name").ToLowerInvariant();
Verb backwardsAccent = new()
{
Text = "admin-smite-lung-removal-name",
Text = backwardsAccentName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
Act = () =>
@@ -760,13 +796,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<BackwardsAccentComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-speak-backwards-description"),
Message = string.Join(": ", backwardsAccentName, Loc.GetString("admin-smite-speak-backwards-description"))
};
args.Verbs.Add(backwardsAccent);
var disarmProneName = Loc.GetString("admin-smite-disarm-prone-name").ToLowerInvariant();
Verb disarmProne = new()
{
Text = "admin-smite-disarm-prone-name",
Text = disarmProneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
Act = () =>
@@ -774,10 +811,11 @@ public sealed partial class AdminVerbSystem
EnsureComp<DisarmProneComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-disarm-prone-description"),
Message = string.Join(": ", disarmProneName, Loc.GetString("admin-smite-disarm-prone-description"))
};
args.Verbs.Add(disarmProne);
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
Verb superSpeed = new()
{
Text = "admin-smite-garbage-can-name",
@@ -792,41 +830,45 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-super-speed-description"),
Message = string.Join(": ", superSpeedName, Loc.GetString("admin-smite-super-speed-description"))
};
args.Verbs.Add(superSpeed);
//Bonk
var superBonkLiteName = Loc.GetString("admin-smite-super-bonk-lite-name").ToLowerInvariant();
Verb superBonkLite = new()
{
Text = "admin-smite-super-bonk-name",
Text = superBonkLiteName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/glass.rsi"), "full"),
Act = () =>
{
_superBonkSystem.StartSuperBonk(args.Target, stopWhenDead: true);
},
Message = Loc.GetString("admin-smite-super-bonk-lite-description"),
Impact = LogImpact.Extreme,
Message = string.Join(": ", superBonkLiteName, Loc.GetString("admin-smite-super-bonk-lite-description"))
};
args.Verbs.Add(superBonkLite);
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
Verb superBonk= new()
{
Text = "admin-smite-super-bonk-lite-name",
Text = superBonkName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/generic.rsi"), "full"),
Act = () =>
{
_superBonkSystem.StartSuperBonk(args.Target);
},
Message = Loc.GetString("admin-smite-super-bonk-description"),
Impact = LogImpact.Extreme,
Message = string.Join(": ", superBonkName, Loc.GetString("admin-smite-super-bonk-description"))
};
args.Verbs.Add(superBonk);
var superslipName = Loc.GetString("admin-smite-super-slip-name").ToLowerInvariant();
Verb superslip = new()
{
Text = "admin-smite-super-slip-name",
Text = superslipName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Janitorial/soap.rsi"), "omega-4"),
Act = () =>
@@ -846,7 +888,7 @@ public sealed partial class AdminVerbSystem
}
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-super-slip-description")
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
};
args.Verbs.Add(superslip);
}

View File

@@ -52,7 +52,6 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -327,7 +326,7 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
var (mapUid, gridUid) = _adminTestArenaSystem.AssertArenaLoaded(player);
_xformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
_transformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-send-to-test-arena-description"),
@@ -533,7 +532,7 @@ public sealed partial class AdminVerbSystem
if (shuttle is null)
return;
_xformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
_transformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
},
Impact = LogImpact.Low,
Message = Loc.GetString("admin-trick-locate-cargo-shuttle-description"),

View File

@@ -35,10 +35,10 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Shared.Silicons.Laws;
using Content.Shared.Movement.Components;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Content.Shared.Mind;
using Content.Shared.Silicons.StationAi;
using Robust.Shared.Physics.Components;
using static Content.Shared.Configurable.ConfigurationComponent;
@@ -63,7 +63,6 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
@@ -294,7 +293,7 @@ namespace Content.Server.Administration.Systems
Act = () =>
{
var ui = new AdminLogsEui();
_eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
ui.SetLogFilter(search:args.Target.Id.ToString());
},
Impact = LogImpact.Low
@@ -348,7 +347,30 @@ namespace Content.Server.Administration.Systems
Impact = LogImpact.Low
});
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
// This logic is needed to be able to modify the AI's laws through its core and eye.
EntityUid? target = null;
SiliconLawBoundComponent? lawBoundComponent = null;
if (TryComp(args.Target, out lawBoundComponent))
{
target = args.Target;
}
// When inspecting the core we can find the entity with its laws by looking at the AiHolderComponent.
else if (TryComp<StationAiHolderComponent>(args.Target, out var holder) && holder.Slot.Item != null
&& TryComp(holder.Slot.Item, out lawBoundComponent))
{
target = holder.Slot.Item.Value;
// For the eye we can find the entity with its laws as the source of the movement relay since the eye
// is just a proxy for it to move around and look around the station.
}
else if (TryComp<MovementRelayTargetComponent>(args.Target, out var relay)
&& TryComp(relay.Source, out lawBoundComponent))
{
target = relay.Source;
}
if (lawBoundComponent != null && target != null)
{
args.Verbs.Add(new Verb()
{
@@ -362,7 +384,7 @@ namespace Content.Server.Administration.Systems
return;
}
_euiManager.OpenEui(ui, session);
ui.UpdateLaws(lawBoundComponent, args.Target);
ui.UpdateLaws(lawBoundComponent, target.Value);
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});

View File

@@ -15,6 +15,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players.RateLimiting;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
@@ -46,20 +47,23 @@ namespace Content.Server.Administration.Systems
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
private static partial Regex DiscordRegex();
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private string _webhookUrl = string.Empty;
private WebhookData? _webhookData;
private string _onCallUrl = string.Empty;
private WebhookData? _onCallData;
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private string _footerIconUrl = string.Empty;
private string _avatarUrl = string.Empty;
private string _serverName = string.Empty;
private readonly
Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel
lastRunLevel)> _relayMessages = new();
private readonly Dictionary<NetUserId, DiscordRelayInteraction> _relayMessages = new();
private Dictionary<NetUserId, string> _oldMessageIds = new();
private readonly Dictionary<NetUserId, Queue<string>> _messageQueues = new();
private readonly Dictionary<NetUserId, Queue<DiscordRelayedData>> _messageQueues = new();
private readonly HashSet<NetUserId> _processingChannels = new();
private readonly Dictionary<NetUserId, (TimeSpan Timestamp, bool Typing)> _typingUpdateTimestamps = new();
private string _overrideClientName = string.Empty;
@@ -81,12 +85,16 @@ namespace Content.Server.Administration.Systems
public override void Initialize()
{
base.Initialize();
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpFooterIcon, OnFooterIconChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpAvatar, OnAvatarChanged, true);
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
var defaultParams = new AHelpMessageParams(
string.Empty,
string.Empty,
@@ -95,7 +103,7 @@ namespace Content.Server.Administration.Systems
_gameTicker.RunLevel,
playedSound: false
);
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Length;
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Message.Length;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
@@ -104,12 +112,37 @@ namespace Content.Server.Administration.Systems
_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
CVarLimitCount = CCVars.AhelpRateLimitCount,
PlayerLimitedAction = PlayerRateLimitedAction
});
new RateLimitRegistration(CCVars.AhelpRateLimitPeriod,
CCVars.AhelpRateLimitCount,
PlayerRateLimitedAction)
);
}
private async void OnCallChanged(string url)
{
_onCallUrl = url;
if (url == string.Empty)
return;
var match = DiscordRegex().Match(url);
if (!match.Success)
{
Log.Error("On call URL does not appear to be valid.");
return;
}
if (match.Groups.Count <= 2)
{
Log.Error("Could not get webhook ID or token for on call URL.");
return;
}
var webhookId = match.Groups[1].Value;
var webhookToken = match.Groups[2].Value;
_onCallData = await GetWebhookData(webhookId, webhookToken);
}
private void PlayerRateLimitedAction(ICommonSession obj)
@@ -260,13 +293,13 @@ namespace Content.Server.Administration.Systems
// Store the Discord message IDs of the previous round
_oldMessageIds = new Dictionary<NetUserId, string>();
foreach (var message in _relayMessages)
foreach (var (user, interaction) in _relayMessages)
{
var id = message.Value.id;
var id = interaction.Id;
if (id == null)
return;
_oldMessageIds[message.Key] = id;
_oldMessageIds[user] = id;
}
_relayMessages.Clear();
@@ -331,10 +364,10 @@ namespace Content.Server.Administration.Systems
var webhookToken = match.Groups[2].Value;
// Fire and forget
await SetWebhookData(webhookId, webhookToken);
_webhookData = await GetWebhookData(webhookId, webhookToken);
}
private async Task SetWebhookData(string id, string token)
private async Task<WebhookData?> GetWebhookData(string id, string token)
{
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
@@ -343,10 +376,10 @@ namespace Content.Server.Administration.Systems
{
_sawmill.Log(LogLevel.Error,
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
return;
return null;
}
_webhookData = JsonSerializer.Deserialize<WebhookData>(content);
return JsonSerializer.Deserialize<WebhookData>(content);
}
private void OnFooterIconChanged(string url)
@@ -359,14 +392,14 @@ namespace Content.Server.Administration.Systems
_avatarUrl = url;
}
private async void ProcessQueue(NetUserId userId, Queue<string> messages)
private async void ProcessQueue(NetUserId userId, Queue<DiscordRelayedData> messages)
{
// Whether an embed already exists for this player
var exists = _relayMessages.TryGetValue(userId, out var existingEmbed);
// Whether the message will become too long after adding these new messages
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Length, MessageLengthCap) + "\n".Length)
+ existingEmbed.description.Length > DescriptionMax;
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Message.Length, MessageLengthCap) + "\n".Length)
+ existingEmbed?.Description.Length > DescriptionMax;
// If there is no existing embed, or it is getting too long, we create a new embed
if (!exists || tooLong)
@@ -386,10 +419,10 @@ namespace Content.Server.Administration.Systems
// If we have all the data required, we can link to the embed of the previous round or embed that was too long
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
{
if (tooLong && existingEmbed.id != null)
if (tooLong && existingEmbed?.Id != null)
{
linkToPrevious =
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**\n";
}
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
{
@@ -399,13 +432,22 @@ namespace Content.Server.Administration.Systems
}
var characterName = _minds.GetCharacterName(userId);
existingEmbed = (null, lookup.Username, linkToPrevious, characterName, _gameTicker.RunLevel);
existingEmbed = new DiscordRelayInteraction()
{
Id = null,
CharacterName = characterName,
Description = linkToPrevious,
Username = lookup.Username,
LastRunLevel = _gameTicker.RunLevel,
};
_relayMessages[userId] = existingEmbed;
}
// Previous message was in another RunLevel, so show that in the embed
if (existingEmbed.lastRunLevel != _gameTicker.RunLevel)
if (existingEmbed!.LastRunLevel != _gameTicker.RunLevel)
{
existingEmbed.description += _gameTicker.RunLevel switch
existingEmbed.Description += _gameTicker.RunLevel switch
{
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
@@ -414,26 +456,35 @@ namespace Content.Server.Administration.Systems
$"{_gameTicker.RunLevel} was not matched."),
};
existingEmbed.lastRunLevel = _gameTicker.RunLevel;
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
}
// If last message of the new batch is SOS then relay it to on-call.
// ... as long as it hasn't been relayed already.
var discordMention = messages.Last();
var onCallRelay = !discordMention.Receivers && !existingEmbed.OnCall;
// Add available messages to the embed description
while (messages.TryDequeue(out var message))
{
// In case someone thinks they're funny
if (message.Length > MessageLengthCap)
message = message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
string text;
existingEmbed.description += $"\n{message}";
// In case someone thinks they're funny
if (message.Message.Length > MessageLengthCap)
text = message.Message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
else
text = message.Message;
existingEmbed.Description += $"\n{text}";
}
var payload = GeneratePayload(existingEmbed.description,
existingEmbed.username,
existingEmbed.characterName);
var payload = GeneratePayload(existingEmbed.Description,
existingEmbed.Username,
existingEmbed.CharacterName);
// If there is no existing embed, create a new one
// Otherwise patch (edit) it
if (existingEmbed.id == null)
if (existingEmbed.Id == null)
{
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
@@ -456,11 +507,11 @@ namespace Content.Server.Administration.Systems
return;
}
existingEmbed.id = id.ToString();
existingEmbed.Id = id.ToString();
}
else
{
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.id}",
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.Id}",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
if (!request.IsSuccessStatusCode)
@@ -475,6 +526,43 @@ namespace Content.Server.Administration.Systems
_relayMessages[userId] = existingEmbed;
// Actually do the on call relay last, we just need to grab it before we dequeue every message above.
if (onCallRelay &&
_onCallData != null)
{
existingEmbed.OnCall = true;
var roleMention = _config.GetCVar(CCVars.DiscordAhelpMention);
if (!string.IsNullOrEmpty(roleMention))
{
var message = new StringBuilder();
message.AppendLine($"<@&{roleMention}>");
message.AppendLine("Unanswered SOS");
// Need webhook data to get the correct link for that channel rather than on-call data.
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
{
message.AppendLine(
$"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**");
}
payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName);
var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
var content = await request.Content.ReadAsStringAsync();
if (!request.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting relay message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
}
}
}
else
{
existingEmbed.OnCall = false;
}
_processingChannels.Remove(userId);
}
@@ -653,7 +741,7 @@ namespace Content.Server.Administration.Systems
if (sendsWebhook)
{
if (!_messageQueues.ContainsKey(msg.UserId))
_messageQueues[msg.UserId] = new Queue<string>();
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
var str = message.Text;
var unameLength = senderSession.Name.Length;
@@ -702,7 +790,7 @@ namespace Content.Server.Administration.Systems
.ToList();
}
private static string GenerateAHelpMessage(AHelpMessageParams parameters)
private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
{
var stringbuilder = new StringBuilder();
@@ -719,13 +807,57 @@ namespace Content.Server.Administration.Systems
stringbuilder.Append($" **{parameters.RoundTime}**");
if (!parameters.PlayedSound)
stringbuilder.Append(" **(S)**");
if (parameters.Icon == null)
stringbuilder.Append($" **{parameters.Username}:** ");
else
stringbuilder.Append($" **{parameters.Username}** ");
stringbuilder.Append(parameters.Message);
return stringbuilder.ToString();
return new DiscordRelayedData()
{
Receivers = !parameters.NoReceivers,
Message = stringbuilder.ToString(),
};
}
private record struct DiscordRelayedData
{
/// <summary>
/// Was anyone online to receive it.
/// </summary>
public bool Receivers;
/// <summary>
/// What's the payload to send to discord.
/// </summary>
public string Message;
}
/// <summary>
/// Class specifically for holding information regarding existing Discord embeds
/// </summary>
private sealed class DiscordRelayInteraction
{
public string? Id;
public string Username = String.Empty;
public string? CharacterName;
/// <summary>
/// Contents for the discord message.
/// </summary>
public string Description = string.Empty;
/// <summary>
/// Run level of the last interaction. If different we'll link to the last Id.
/// </summary>
public GameRunLevel LastRunLevel;
/// <summary>
/// Did we relay this interaction to OnCall previously.
/// </summary>
public bool OnCall;
}
}

View File

@@ -25,6 +25,16 @@ public sealed class TagCommand : ToolshedCommand
});
}
[CommandImplementation("with")]
public IEnumerable<EntityUid> With(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> entities,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> tag)
{
_tag ??= GetSys<TagSystem>();
return entities.Where(e => _tag.HasTag(e, tag.Evaluate(ctx)!));
}
[CommandImplementation("add")]
public EntityUid Add(
[CommandInvocationContext] IInvocationContext ctx,

View File

@@ -1,7 +1,19 @@
using Content.Shared.Alert;
using Robust.Shared.GameStates;
namespace Content.Server.Alert;
internal sealed class ServerAlertsSystem : AlertsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AlertsComponent, ComponentGetState>(OnGetState);
}
private void OnGetState(Entity<AlertsComponent> alerts, ref ComponentGetState args)
{
args.State = new AlertComponentState(alerts.Comp.Alerts);
}
}

View File

@@ -55,7 +55,7 @@ public sealed partial class AmeControllerComponent : SharedAmeControllerComponen
/// </summary>
[DataField("injectSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier InjectSound = new SoundCollectionSpecifier("MetalThud");
public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Machines/ame_fuelinjection.ogg");
/// <summary>
/// The last time this could have injected fuel into the AME.

View File

@@ -22,11 +22,17 @@ public sealed class TechAnomalySystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<TechAnomalyComponent, MapInitEvent>(OnTechMapInit);
SubscribeLocalEvent<TechAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<TechAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
SubscribeLocalEvent<TechAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
}
private void OnTechMapInit(Entity<TechAnomalyComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextTimer = _timing.CurTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View File

@@ -11,7 +11,6 @@ using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Shared.Antag;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
@@ -20,7 +19,6 @@ using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
@@ -37,14 +35,14 @@ namespace Content.Server.Antag;
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly LoadoutSystem _loadout = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
@@ -57,6 +55,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
base.Initialize();
Log.Level = LogLevel.Debug;
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
@@ -193,6 +193,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
@@ -209,8 +212,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="def">The antagonist selection parameters and criteria</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def, bool midround = false)
public void ChooseAntags(Entity<AntagSelectionComponent> ent,
IList<ICommonSession> pool,
AntagSelectionDefinition def,
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
@@ -331,7 +340,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
EntityManager.AddComponents(player, def.Components);
// Equip the entity's RoleLoadout and LoadoutGroup
List<ProtoId<StartingGearPrototype>>? gear = new();
List<ProtoId<StartingGearPrototype>> gear = new();
if (def.StartingGear is not null)
gear.Add(def.StartingGear.Value);
@@ -340,8 +349,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
var curMind = session.GetMind();
if (curMind == null ||
if (curMind == null ||
!TryComp<MindComponent>(curMind.Value, out var mindComp) ||
mindComp.OwnedEntity != antagEnt)
{
@@ -350,9 +359,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindComponents, null, true);
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);

View File

@@ -3,7 +3,6 @@ using Content.Shared.Antag;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Player;
@@ -145,10 +144,17 @@ public partial struct AntagSelectionDefinition()
/// <summary>
/// Components added to the player's mind.
/// Do NOT use this to add role-type components. Add those as MindRoles instead
/// </summary>
[DataField]
public ComponentRegistry MindComponents = new();
/// <summary>
/// List of Mind Role Prototypes to be added to the player's mind.
/// </summary>
[DataField]
public List<ProtoId<EntityPrototype>>? MindRoles;
/// <summary>
/// A set of starting gear that's equipped to the player.
/// </summary>

View File

@@ -1,9 +1,11 @@
using Robust.Shared.Prototypes;
namespace Content.Server.Atmos.Components
{
/// <summary>
/// Used by FixGridAtmos. Entities with this may get magically auto-deleted on map initialization in future.
/// </summary>
[RegisterComponent]
[RegisterComponent, EntityCategory("Mapping")]
public sealed partial class AtmosFixMarkerComponent : Component
{
// See FixGridAtmos for more details

View File

@@ -1,15 +1,19 @@
using Content.Server.Atmos.Monitor.Components;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Pinpointer;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Consoles;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
private const float UpdateTime = 1.0f;
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
// Alarm events
SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
}
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
}
private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
{
OnDeviceAdditionOrRemoval(uid, component, args.Anchored);
}
private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args)
{
OnDeviceAdditionOrRemoval(uid, component, false);
}
private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding)
{
var xform = Transform(uid);
var gridUid = xform.GridUid;
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (gridUid == null)
return;
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
return;
var netEntity = EntityManager.GetNetEntity(uid);
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
return;
var netEntity = GetNetEntity(uid);
var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
@@ -99,11 +125,18 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (gridUid != entXform.GridUid)
continue;
if (args.Anchored)
if (isAdding)
{
entConsole.AtmosDevices.Add(data.Value);
}
else if (!args.Anchored)
else
{
entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
_navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity);
}
Dirty(ent, entConsole);
}
}
@@ -209,6 +242,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (entDevice.Group != group)
continue;
if (!TryComp<MapGridComponent>(entXform.GridUid, out var mapGrid))
continue;
if (!TryComp<NavMapComponent>(entXform.GridUid, out var navMap))
continue;
// If emagged, change the alarm type to normal
var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
@@ -216,14 +255,45 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
alarmState = AtmosAlarmType.Invalid;
// Create entry
var netEnt = GetNetEntity(ent);
var entry = new AtmosAlertsComputerEntry
(GetNetEntity(ent),
(netEnt,
GetNetCoordinates(entXform.Coordinates),
entDevice.Group,
alarmState,
MetaData(ent).EntityName,
entDeviceNetwork.Address);
// Get the list of sensors attached to the alarm
var sensorList = TryComp<DeviceListComponent>(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null;
if (sensorList?.Any() == true)
{
var alarmRegionSeeds = new HashSet<Vector2i>();
// If valid and anchored, use the position of sensors as seeds for the region
foreach (var (address, sensorEnt) in sensorList)
{
if (!sensorEnt.IsValid() || !HasComp<AtmosMonitorComponent>(sensorEnt))
continue;
var sensorXform = Transform(sensorEnt);
if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid)
alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform)));
}
var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds);
_navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties);
}
else
{
_navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt);
}
alarmStateData.Add(entry);
}
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
{
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
if (entXform.GridUid != gridUid)
continue;
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data))
atmosDeviceNavMapData.Add(data.Value);
}
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
(EntityUid uid,
AtmosAlertsDeviceComponent component,
TransformComponent xform,
EntityUid gridUid,
[NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
{
output = null;
if (xform.GridUid != gridUid)
return false;
if (!xform.Anchored)
return false;

View File

@@ -281,6 +281,17 @@ namespace Content.Server.Atmos.EntitySystems
return true;
}
/// <summary>
/// Compares two TileAtmospheres to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>
public GasCompareResult CompareExchange(TileAtmosphere sample, TileAtmosphere otherSample)
{
if (sample.AirArchived == null || otherSample.AirArchived == null)
return GasCompareResult.NoExchange;
return CompareExchange(sample.AirArchived, otherSample.AirArchived);
}
/// <summary>
/// Compares two gas mixtures to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>

View File

@@ -270,7 +270,7 @@ public sealed partial class AtmosphereSystem
{
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable == false);
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
var count = 0;

View File

@@ -210,7 +210,7 @@ namespace Content.Server.Atmos.EntitySystems
MovedByPressureComponent.ProbabilityOffset);
// Can we yeet the thing (due to probability, strength, etc.)
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _robustRandom.Prob(MathF.Min(moveProb / 100f, 1f))
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _random.Prob(MathF.Min(moveProb / 100f, 1f))
&& !float.IsPositiveInfinity(component.MoveResist)
&& (physics.BodyType != BodyType.Static
&& (maxForce >= (component.MoveResist * MovedByPressureComponent.MoveForcePushRatio)))

View File

@@ -54,7 +54,7 @@ namespace Content.Server.Atmos.EntitySystems
}
shouldShareAir = true;
} else if (CompareExchange(tile.Air, enemyTile.Air) != GasCompareResult.NoExchange)
} else if (CompareExchange(tile, enemyTile) != GasCompareResult.NoExchange)
{
AddActiveTile(gridAtmosphere, enemyTile);
if (ExcitedGroups)
@@ -117,9 +117,8 @@ namespace Content.Server.Atmos.EntitySystems
private void Archive(TileAtmosphere tile, int fireCount)
{
if (tile.Air != null)
tile.Air.Moles.AsSpan().CopyTo(tile.MolesArchived.AsSpan());
tile.AirArchived = new GasMixture(tile.Air);
tile.TemperatureArchived = tile.Temperature;
tile.ArchivedCycle = fireCount;
}
@@ -184,10 +183,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float GetHeatCapacityArchived(TileAtmosphere tile)
{
if (tile.Air == null)
if (tile.AirArchived == null)
return tile.HeatCapacity;
return GetHeatCapacityCalculation(tile.MolesArchived!, tile.Space);
return GetHeatCapacity(tile.AirArchived);
}
/// <summary>
@@ -195,10 +194,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int atmosAdjacentTurfs)
{
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer)
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
var absTemperatureDelta = Math.Abs(temperatureDelta);
var oldHeatCapacity = 0f;
var oldSharerHeatCapacity = 0f;
@@ -249,12 +249,12 @@ namespace Content.Server.Atmos.EntitySystems
// Transfer of thermal energy (via changed heat capacity) between self and sharer.
if (!receiver.Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.TemperatureArchived) + (heatCapacitySharerToThis * tileSharer.TemperatureArchived)) / newHeatCapacity;
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.AirArchived.Temperature) + (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature)) / newHeatCapacity;
}
if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.TemperatureArchived) + (heatCapacityToSharer * tileReceiver.TemperatureArchived)) / newSharerHeatCapacity;
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) + (heatCapacityToSharer * tileReceiver.AirArchived.Temperature)) / newSharerHeatCapacity;
}
// Thermal energy of the system (self and sharer) is unchanged.
@@ -273,7 +273,7 @@ namespace Content.Server.Atmos.EntitySystems
var moles = receiver.TotalMoles;
var theirMoles = sharer.TotalMoles;
return (tileReceiver.TemperatureArchived * (moles + movedMoles)) - (tileSharer.TemperatureArchived * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
return (tileReceiver.AirArchived.Temperature * (moles + movedMoles)) - (tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
}
/// <summary>
@@ -281,10 +281,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, float conductionCoefficient)
{
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer)
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);
@@ -310,10 +311,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, float conductionCoefficient, float sharerTemperature, float sharerHeatCapacity)
{
if (tileReceiver.Air is not {} receiver)
if (tileReceiver.Air is not {} receiver || tileReceiver.AirArchived == null)
return 0;
var temperatureDelta = tileReceiver.TemperatureArchived - sharerTemperature;
var temperatureDelta = tileReceiver.AirArchived.Temperature - sharerTemperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);

View File

@@ -355,7 +355,7 @@ namespace Content.Server.Atmos.EntitySystems
continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.Air != null && CompareExchange(otherTile2.Air, tile.Air) == GasCompareResult.NoExchange)
if (otherTile2.Air != null && CompareExchange(otherTile2, tile) == GasCompareResult.NoExchange)
continue;
AddActiveTile(gridAtmosphere, otherTile2);
@@ -689,7 +689,7 @@ namespace Content.Server.Atmos.EntitySystems
var chance = MathHelper.Clamp(0.01f + (sum / SpacingMaxWind) * 0.3f, 0.003f, 0.3f);
if (sum > 20 && _robustRandom.Prob(chance))
if (sum > 20 && _random.Prob(chance))
PryTile(mapGrid, tile.GridIndices);
}

View File

@@ -231,7 +231,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.MapAtmosphere = false;
atmos.MapTiles.Remove(tile);
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Space = false;
@@ -261,7 +261,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();

View File

@@ -131,7 +131,10 @@ namespace Content.Server.Atmos.EntitySystems
private void TemperatureShareMutualSolid(TileAtmosphere tile, TileAtmosphere other, float conductionCoefficient)
{
var deltaTemperature = (tile.TemperatureArchived - other.TemperatureArchived);
if (tile.AirArchived == null || other.AirArchived == null)
return;
var deltaTemperature = (tile.AirArchived.Temperature - other.AirArchived.Temperature);
if (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider
&& tile.HeatCapacity != 0f && other.HeatCapacity != 0f)
{
@@ -145,11 +148,14 @@ namespace Content.Server.Atmos.EntitySystems
public void RadiateToSpace(TileAtmosphere tile)
{
if (tile.AirArchived == null)
return;
// Considering 0ºC as the break even point for radiation in and out.
if (tile.Temperature > Atmospherics.T0C)
{
// Hardcoded space temperature.
var deltaTemperature = (tile.TemperatureArchived - Atmospherics.TCMB);
var deltaTemperature = (tile.AirArchived.Temperature - Atmospherics.TCMB);
if ((tile.HeatCapacity > 0) && (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider))
{
var heat = tile.ThermalConductivity * deltaTemperature * (tile.HeatCapacity *

View File

@@ -14,7 +14,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Atmos.EntitySystems;
@@ -27,7 +26,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
@@ -39,7 +37,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] public readonly PuddleSystem Puddle = default!;
private const float ExposedUpdateDelay = 1f;
@@ -124,6 +121,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private void CacheDecals()
{
_burntDecals = _prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
_burntDecals = _protoMan.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
}
}

View File

@@ -22,9 +22,6 @@ namespace Content.Server.Atmos
[ViewVariables]
public float Temperature { get; set; } = Atmospherics.T20C;
[ViewVariables]
public float TemperatureArchived { get; set; } = Atmospherics.T20C;
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget { get; set; }
@@ -93,12 +90,16 @@ namespace Content.Server.Atmos
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture? Air { get; set; }
/// <summary>
/// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air
/// in-place without affecting the results based on update order.
/// </summary>
[ViewVariables]
public GasMixture? AirArchived;
[DataField("lastShare")]
public float LastShare;
[ViewVariables]
public readonly float[] MolesArchived = new float[Atmospherics.AdjustedNumberOfGases];
GasMixture IGasMixtureHolder.Air
{
get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
@@ -139,6 +140,7 @@ namespace Content.Server.Atmos
GridIndex = gridIndex;
GridIndices = gridIndices;
Air = mixture;
AirArchived = Air != null ? Air.Clone() : null;
Space = space;
if(immutable)
@@ -153,7 +155,7 @@ namespace Content.Server.Atmos
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
Array.Copy(other.MolesArchived, MolesArchived, MolesArchived.Length);
AirArchived = Air != null ? Air.Clone() : null;
}
public TileAtmosphere()

View File

@@ -239,7 +239,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
Loc.GetString(
"earlyleave-cryo-announcement",
("character", name),
("entity", ent.Owner),
("entity", ent.Owner), // gender things for supporting downstreams with other languages
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
), Loc.GetString("earlyleave-cryo-sender"),
playDefaultSound: false

View File

@@ -472,7 +472,7 @@ public sealed class BloodstreamSystem : EntitySystem
return;
}
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume);
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
component.BloodReagent = reagent;

View File

@@ -14,7 +14,6 @@ public sealed class LungSystem : EntitySystem
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public static string LungSolutionName = "Lung";
@@ -29,7 +28,7 @@ public sealed class LungSystem : EntitySystem
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
{
_atmosphereSystem.DisconnectInternals(ent);
_atmos.DisconnectInternals(ent);
}
private void OnGotEquipped(Entity<BreathToolComponent> ent, ref GotEquippedEvent args)
@@ -93,7 +92,7 @@ public sealed class LungSystem : EntitySystem
if (moles <= 0)
continue;
var reagent = _atmosphereSystem.GasReagents[i];
var reagent = _atmos.GasReagents[i];
if (reagent is null)
continue;

View File

@@ -1,5 +1,6 @@
using Content.Shared.Chemistry.Components;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Audio;
namespace Content.Server.Botany.Components;
@@ -23,6 +24,9 @@ public sealed partial class PlantHolderComponent : Component
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
[DataField]
public SoundSpecifier? WateringSound;
[DataField]
public bool UpdateSpriteAfterUpdate;

View File

@@ -27,7 +27,7 @@ public sealed class MutationSystem : EntitySystem
{
foreach (var mutation in _randomMutations.mutations)
{
if (Random(mutation.BaseOdds * severity))
if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f)))
{
if (mutation.AppliesToPlant)
{

View File

@@ -1,6 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Fluids.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
@@ -18,7 +17,6 @@ using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -37,7 +35,6 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
@@ -53,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<PlantHolderComponent, SolutionTransferredEvent>(OnSolutionTransferred);
}
public override void Update(float frameTime)
@@ -158,6 +156,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (!_botany.TryGetSeed(seeds, out var seed))
return;
args.Handled = true;
var name = Loc.GetString(seed.Name);
var noun = Loc.GetString(seed.Noun);
_popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
@@ -185,6 +184,7 @@ public sealed class PlantHolderSystem : EntitySystem
return;
}
args.Handled = true;
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
return;
@@ -192,6 +192,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (_tagSystem.HasTag(args.Used, "Hoe"))
{
args.Handled = true;
if (component.WeedLevel > 0)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
@@ -211,6 +212,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (HasComp<ShovelComponent>(args.Used))
{
args.Handled = true;
if (component.Seed != null)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
@@ -228,39 +230,9 @@ public sealed class PlantHolderSystem : EntitySystem
return;
}
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
&& TryComp(args.Used, out SprayComponent? spray))
{
var amount = FixedPoint2.New(1);
var targetEntity = uid;
var solutionEntity = args.Used;
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
if (split.Volume == 0)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
("owner", args.Used)), args.User);
return;
}
_popup.PopupCursor(Loc.GetString("plant-holder-component-spray-message",
("owner", uid),
("amount", split.Volume)), args.User, PopupType.Medium);
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
ForceUpdateByExternalCause(uid, component);
return;
}
if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
{
args.Handled = true;
if (component.Seed == null)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
@@ -316,10 +288,15 @@ public sealed class PlantHolderSystem : EntitySystem
}
if (HasComp<SharpComponent>(args.Used))
{
args.Handled = true;
DoHarvest(uid, args.User, component);
return;
}
if (TryComp<ProduceComponent>(args.Used, out var produce))
{
args.Handled = true;
_popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
("owner", uid),
("usingItem", args.Used)), args.User, PopupType.Medium);
@@ -351,6 +328,10 @@ public sealed class PlantHolderSystem : EntitySystem
}
}
private void OnSolutionTransferred(Entity<PlantHolderComponent> ent, ref SolutionTransferredEvent args)
{
_audio.PlayPvs(ent.Comp.WateringSound, ent.Owner);
}
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
{
DoHarvest(entity, args.User, entity.Comp);
@@ -699,7 +680,10 @@ public sealed class PlantHolderSystem : EntitySystem
if (TryComp<HandsComponent>(user, out var hands))
{
if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
return false;
}
}
else if (!_botany.CanHarvest(component.Seed))
{

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Cargo.Components;
/// <summary>
/// This is used for the price gun, which calculates the price of any object it appraises.
/// </summary>
[RegisterComponent]
public sealed partial class PriceGunComponent : Component
{
}

View File

@@ -420,6 +420,13 @@ public sealed partial class CargoSystem
return false;
_nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
var newBounty = new CargoBountyData(bounty, randomVal);
// This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
if (component.Bounties.Any(b => b.Id == newBounty.Id))
{
Log.Error("Failed to add bounty {ID} because another one with the same ID already existed!", newBounty.Id);
return false;
}
component.Bounties.Add(new CargoBountyData(bounty, randomVal));
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
component.TotalBounties++;

View File

@@ -1,73 +1,34 @@
using Content.Server.Cargo.Components;
using Content.Server.Popups;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Cargo.Systems;
namespace Content.Server.Cargo.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class PriceGunSystem : EntitySystem
public sealed class PriceGunSystem : SharedPriceGunSystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly CargoSystem _bountySystem = default!;
/// <inheritdoc/>
public override void Initialize()
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
{
SubscribeLocalEvent<PriceGunComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<PriceGunComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}
private void OnUtilityVerb(EntityUid uid, PriceGunComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
var price = _pricingSystem.GetPrice(args.Target);
var verb = new UtilityVerb()
{
Act = () =>
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
_useDelay.TryResetDelay((uid, useDelay));
},
Text = Loc.GetString("price-gun-verb-text"),
Message = Loc.GetString("price-gun-verb-message", ("object", Identity.Entity(args.Target, EntityManager)))
};
args.Verbs.Add(verb);
}
private void OnAfterInteract(EntityUid uid, PriceGunComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null || args.Handled)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
return false;
// Check if we're scanning a bounty crate
if (_bountySystem.IsBountyComplete(args.Target.Value, out _))
if (_bountySystem.IsBountyComplete(target, out _))
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), args.User, args.User);
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), user, user);
}
else // Otherwise appraise the price
{
double price = _pricingSystem.GetPrice(args.Target.Value);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target.Value, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
var price = _pricingSystem.GetPrice(target);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(target, EntityManager)), ("price", $"{price:F2}")), user, user);
}
_useDelay.TryResetDelay((uid, useDelay));
args.Handled = true;
_useDelay.TryResetDelay((priceGunUid, useDelay));
return true;
}
}

View File

@@ -428,6 +428,7 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
var cartridgeEvent = args.MessageEvent;
cartridgeEvent.LoaderUid = GetNetEntity(uid);
cartridgeEvent.Actor = args.Actor;
RelayEvent(component, cartridgeEvent, true);
}

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