Merge remote-tracking branch 'upstream/stable' into ed-29-10-2024-upstream

# Conflicts:
#	Content.Server/Chat/Managers/ChatSanitizationManager.cs
#	Content.Server/Temperature/Systems/TemperatureSystem.cs
#	Content.Shared/Localizations/ContentLocalizationManager.cs
This commit is contained in:
Ed
2024-10-29 11:16:56 +03:00
388 changed files with 34535 additions and 18822 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

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

@@ -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;
@@ -71,6 +72,7 @@ namespace Content.Client.Entry
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
public override void Init()
{
@@ -140,6 +142,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 +168,7 @@ namespace Content.Client.Entry
_userInterfaceManager.SetDefaultTheme(_configManager.GetCVar(CCVars.UIDefaultInterfaceTheme));
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
_documentParsingManager.Initialize();
_titleWindowManager.Initialize();
_baseClient.RunLevelChanged += (_, args) =>
{

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

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

@@ -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;
@@ -57,6 +58,7 @@ namespace Content.Client.IoC
collection.Register<DebugMonitorManager>();
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
}
}
}

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

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

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

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

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

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

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

@@ -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,10 +642,11 @@ 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",
@@ -629,13 +657,14 @@ public sealed partial class AdminVerbSystem
_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

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

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

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

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

View File

@@ -1,15 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
namespace Content.Server.Chat.Managers;
/// <summary>
/// Sanitizes messages!
/// It currently ony removes the shorthands for emotes (like "lol" or "^-^") from a chat message and returns the last
/// emote in their message
/// </summary>
public sealed class ChatSanitizationManager : IChatSanitizationManager
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private static readonly Dictionary<string, string> SmileyToEmote = new()
private static readonly Dictionary<string, string> ShorthandToEmote = new()
{
// CP14-RU-Localization-Start
{ "лол", "chatsan-laughs" },
@@ -60,7 +63,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ ":D", "chatsan-smiles-widely" },
{ "D:", "chatsan-frowns-deeply" },
{ ":O", "chatsan-surprised" },
{ ":3", "chatsan-smiles" }, //nope
{ ":3", "chatsan-smiles" },
{ ":S", "chatsan-uncertain" },
{ ":>", "chatsan-grins" },
{ ":<", "chatsan-pouts" },
@@ -102,7 +105,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ "kek", "chatsan-laughs" },
{ "rofl", "chatsan-laughs" },
{ "o7", "chatsan-salutes" },
{ ";_;7", "chatsan-tearfully-salutes"},
{ ";_;7", "chatsan-tearfully-salutes" },
{ "idk", "chatsan-shrugs" },
{ ";)", "chatsan-winks" },
{ ";]", "chatsan-winks" },
@@ -115,9 +118,12 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ "(':", "chatsan-tearfully-smiles" },
{ "[':", "chatsan-tearfully-smiles" },
{ "('=", "chatsan-tearfully-smiles" },
{ "['=", "chatsan-tearfully-smiles" },
{ "['=", "chatsan-tearfully-smiles" }
};
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private bool _doSanitize;
public void Initialize()
@@ -125,29 +131,60 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
_configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => _doSanitize = x, true);
}
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote)
/// <summary>
/// Remove the shorthands from the message, returning the last one found as the emote
/// </summary>
/// <param name="message">The pre-sanitized message</param>
/// <param name="speaker">The speaker</param>
/// <param name="sanitized">The sanitized message with shorthands removed</param>
/// <param name="emote">The localized emote</param>
/// <returns>True if emote has been sanitized out</returns>
public bool TrySanitizeEmoteShorthands(string message,
EntityUid speaker,
out string sanitized,
[NotNullWhen(true)] out string? emote)
{
if (!_doSanitize)
{
sanitized = input;
emote = null;
return false;
}
input = input.TrimEnd();
foreach (var (smiley, replacement) in SmileyToEmote)
{
if (input.EndsWith(smiley, true, CultureInfo.InvariantCulture))
{
sanitized = input.Remove(input.Length - smiley.Length).TrimEnd();
emote = Loc.GetString(replacement, ("ent", speaker));
return true;
}
}
sanitized = input;
emote = null;
return false;
sanitized = message;
if (!_doSanitize)
return false;
// -1 is just a canary for nothing found yet
var lastEmoteIndex = -1;
foreach (var (shorthand, emoteKey) in ShorthandToEmote)
{
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern =
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
// We're using sanitized as the original message until the end so that we can make sure the indices of
// the emotes are accurate.
var lastMatch = r.Match(sanitized);
if (!lastMatch.Success)
continue;
if (lastMatch.Index > lastEmoteIndex)
{
lastEmoteIndex = lastMatch.Index;
emote = _loc.GetString(emoteKey, ("ent", speaker));
}
message = r.Replace(message, string.Empty);
}
sanitized = message.Trim();
return emote is not null;
}
}

View File

@@ -6,5 +6,8 @@ public interface IChatSanitizationManager
{
public void Initialize();
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote);
public bool TrySanitizeEmoteShorthands(string input,
EntityUid speaker,
out string sanitized,
[NotNullWhen(true)] out string? emote);
}

View File

@@ -746,8 +746,12 @@ public sealed partial class ChatSystem : SharedChatSystem
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
{
var newMessage = message.Trim();
newMessage = SanitizeMessageReplaceWords(newMessage);
var newMessage = SanitizeMessageReplaceWords(message.Trim());
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
// Sanitize it first as it might change the word order
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
if (capitalize)
newMessage = SanitizeMessageCapital(newMessage);
@@ -756,9 +760,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
return newMessage;
return prefix + newMessage;
}
private string SanitizeInGameOOCMessage(string message)

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Used for embeddable entities that should try to inject a
/// contained solution into a target over time while they are embbeded into.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
///<summary>
///The time at which the injection will happen.
///</summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextUpdate;
///<summary>
///The delay between each injection in seconds.
///</summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
}
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
}
private void OnInjectOverTime(Entity<SolutionInjectWhileEmbeddedComponent> entity, ref InjectOverTimeEvent args)
{
DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
}
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
{
TryInjectTargets(injectorEntity, [target], source);

View File

@@ -0,0 +1,60 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Collections;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.EntitySystems;
/// <summary>
/// System for handling injecting into an entity while a projectile is embedded.
/// </summary>
public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<SolutionInjectWhileEmbeddedComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SolutionInjectWhileEmbeddedComponent, EmbeddableProjectileComponent>();
while (query.MoveNext(out var uid, out var injectComponent, out var projectileComponent))
{
if (_gameTiming.CurTime < injectComponent.NextUpdate)
continue;
injectComponent.NextUpdate += injectComponent.UpdateInterval;
if(projectileComponent.EmbeddedIntoUid == null)
continue;
var ev = new InjectOverTimeEvent(projectileComponent.EmbeddedIntoUid.Value);
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -875,10 +875,41 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
public async Task AddAdminLogs(List<AdminLog> logs)
{
const int maxRetryAttempts = 5;
var initialRetryDelay = TimeSpan.FromSeconds(5);
DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
await using var db = await GetDb();
db.DbContext.AdminLog.AddRange(logs);
await db.DbContext.SaveChangesAsync();
var attempt = 0;
var retryDelay = initialRetryDelay;
while (attempt < maxRetryAttempts)
{
try
{
await using var db = await GetDb();
db.DbContext.AdminLog.AddRange(logs);
await db.DbContext.SaveChangesAsync();
_opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
break;
}
catch (Exception ex)
{
attempt += 1;
_opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
if (attempt >= maxRetryAttempts)
{
_opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
return;
}
_opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
await Task.Delay(retryDelay);
retryDelay *= 2;
}
}
}
protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);

View File

@@ -26,9 +26,17 @@ public sealed partial class JobCondition : EntityEffectCondition
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
|| mindRole.JobPrototype is null)
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
{
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
continue;
}
if (mindRole.JobPrototype == null)
{
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
continue;
}
if (Job.Contains(mindRole.JobPrototype.Value))
return true;

View File

@@ -192,9 +192,6 @@ namespace Content.Server.GameTicking
if (!_playerManager.TryGetSessionById(userId, out _))
continue;
if (_banManager.GetRoleBans(userId) == null)
continue;
total++;
}
@@ -238,11 +235,7 @@ namespace Content.Server.GameTicking
#if DEBUG
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
#endif
if (_banManager.GetRoleBans(userId) == null)
{
Logger.ErrorS("RoleBans", $"Role bans for player {session} {userId} have not been loaded yet.");
continue;
}
readyPlayers.Add(session);
HumanoidCharacterProfile profile;
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))

View File

@@ -1,4 +1,5 @@
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Content.Shared.NPC.Prototypes;
using Content.Shared.Random;
using Content.Shared.Roles;
@@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component
[DataField]
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give this traitor an Uplink on spawn.
/// </summary>
[DataField]
public bool GiveUplink = true;
/// <summary>
/// Give this traitor the codewords.
/// </summary>
[DataField]
public bool GiveCodewords = true;
/// <summary>
/// Give this traitor a briefing in chat.
/// </summary>
[DataField]
public bool GiveBriefing = true;
public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3];
@@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
/// The amount of TC traitors start with.
/// </summary>
[DataField]
public int StartingBalance = 20;
public FixedPoint2 StartingBalance = 20;
}

View File

@@ -155,8 +155,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out _, out var role))
role.Value.Comp.ConvertedCount++;
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
role.Value.Comp2.ConvertedCount++;
}
}

View File

@@ -7,6 +7,7 @@ using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
@@ -75,38 +76,46 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return codewords;
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{
//Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
return false;
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
var briefing = "";
if (component.GiveCodewords)
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
// Uplink code will go here if applicable, but we still need the variable if there aren't any
Note[]? code = null;
if (giveUplink)
if (component.GiveUplink)
{
// Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
{
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - prototype.AntagAdvantage;
}
// creadth: we need to create uplink for the antag.
// PDA should be in place already
var pda = _uplink.FindUplinkTarget(traitor);
if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true))
return false;
// Give traitors their codewords and uplink code to keep in their character info menu
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}", briefing,
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
// Choose and generate an Uplink, and return the uplink code if applicable
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
code = uplinkParams.Item1;
briefing = uplinkParams.Item2;
}
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
string[]? codewords = null;
if (component.GiveCodewords)
codewords = component.Codewords;
if (component.GiveBriefing)
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId);
@@ -134,6 +143,32 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return true;
}
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
{
var pda = _uplink.FindUplinkTarget(traitor);
Note[]? code = null;
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
if (pda is not null && uplinked)
{
// Codes are only generated if the uplink is a PDA
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}",
briefing,
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
return (code, briefing);
}
else if (pda is null && uplinked)
{
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
}
return (null, briefing);
}
// TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
@@ -141,13 +176,17 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
// TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown"))));
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (codewords != null)
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (uplinkCode != null)
sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
else
sb.AppendLine(Loc.GetString("traitor-role-uplink-implant"));
return sb.ToString();
}

View File

@@ -516,8 +516,8 @@ public sealed class GhostRoleSystem : EntitySystem
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind, out _, out var markerRole))
markerRole.Value.Comp.Name = role.RoleName;
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
_mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob);

View File

@@ -45,7 +45,7 @@ public sealed class HolosignSystem : EntitySystem
if (args.Handled
|| !args.CanReach // prevent placing out of range
|| HasComp<StorageComponent>(args.Target) // if it's a storage component like a bag, we ignore usage so it can be stored
|| !_powerCell.TryUseCharge(uid, component.ChargeUse) // if no battery or no charge, doesn't work
|| !_powerCell.TryUseCharge(uid, component.ChargeUse, user: args.User) // if no battery or no charge, doesn't work
)
return;

View File

@@ -22,6 +22,9 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
// How much the cell score should be increased per 1 AutoRechargeRate.
private const int AutoRechargeValue = 100;
public override void Initialize()
{
base.Initialize();
@@ -59,15 +62,26 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
return;
// no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
return;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge)
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
{
args.Cancel();
return;
}
var user = Transform(uid).ParentUid;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (GetCellScore(inserting.Owner, inserting) <= GetCellScore(battery.Owner, battery))
{
args.Cancel();
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
return;
}
// tell ninja abilities that use battery to update it so they don't use charge from the old one
var user = Transform(uid).ParentUid;
if (!_ninja.IsNinja(user))
return;
@@ -76,6 +90,16 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
RaiseLocalEvent(user, ref ev);
}
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue);
return battcomp.MaxCharge;
}
private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
{
// ninja suit (battery) is immune to emp

View File

@@ -1,14 +1,12 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Roles;
using Content.Shared.Sticky;
using Robust.Shared.GameObjects;
namespace Content.Server.Ninja.Systems;
@@ -19,6 +17,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
{
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
@@ -41,7 +40,10 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
var user = args.User;
if (!_mind.TryGetRole<NinjaRoleComponent>(user, out var _))
if (!_mind.TryGetMind(args.User, out var mind, out _))
return;
if (!_role.MindHasRole<NinjaRoleComponent>(mind))
{
_popup.PopupEntity(Loc.GetString("spider-charge-not-ninja"), user, user);
args.Cancelled = true;

View File

@@ -317,7 +317,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f));
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
_stomach.TryTransferSolution(firstStomach.Value.Owner, drained, firstStomach.Value.Comp1);

View File

@@ -296,7 +296,7 @@ public sealed class FoodSystem : EntitySystem
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f));
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
// Try to break all used utensils
foreach (var utensil in utensils)

View File

@@ -1,9 +1,9 @@
using Content.Server.Objectives.Components;
using Content.Server.Revolutionary.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
@@ -17,7 +17,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
@@ -86,11 +85,10 @@ public sealed class KillPersonConditionSystem : EntitySystem
}
var allHeads = new List<EntityUid>();
foreach (var mind in allHumans)
foreach (var person in allHumans)
{
// RequireAdminNotify used as a cheap way to check for command department
if (_job.MindTryGetJob(mind, out var prototype) && prototype.RequireAdminNotify)
allHeads.Add(mind);
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
allHeads.Add(person);
}
if (allHeads.Count == 0)

View File

@@ -1,13 +1,11 @@
using Content.Server.Objectives.Components;
using Content.Server.Revolutionary.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Objectives.Systems;
public sealed class NotCommandRequirementSystem : EntitySystem
{
[Dependency] private readonly SharedJobSystem _job = default!;
public override void Initialize()
{
base.Initialize();
@@ -20,8 +18,7 @@ public sealed class NotCommandRequirementSystem : EntitySystem
if (args.Cancelled)
return;
// cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
if (_job.MindTryGetJob(args.MindId, out var prototype) && prototype.RequireAdminNotify)
if (args.Mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
args.Cancelled = true;
}
}

View File

@@ -25,9 +25,6 @@ public sealed partial class ApcComponent : BaseApcNetComponent
[DataField("enabled")]
public bool MainBreakerEnabled = true;
// TODO: remove this since it probably breaks when 2 people use it
[DataField("hasAccess")]
public bool HasAccess = false;
/// <summary>
/// APC state needs to always be updated after first processing tick.

View File

@@ -2,7 +2,6 @@ using Content.Server.Emp;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.APC;
using Content.Shared.Emag.Components;
@@ -71,11 +70,8 @@ public sealed class ApcSystem : EntitySystem
component.NeedStateUpdate = true;
}
//Update the HasAccess var for UI to read
private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
{
// TODO: this should be per-player not stored on the apc
component.HasAccess = _accessReader.IsAllowed(args.Actor, uid);
UpdateApcState(uid, component);
}
@@ -165,7 +161,7 @@ public sealed class ApcSystem : EntitySystem
// TODO: Fix ContentHelpers or make a new one coz this is cooked.
var charge = ContentHelpers.RoundToNearestLevels(battery.CurrentStorage / battery.Capacity, 1.0, 100 / ChargeAccuracy) / 100f * ChargeAccuracy;
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
charge);

View File

@@ -1,11 +1,9 @@
using Content.Server.GameTicking.Rules;
namespace Content.Server.Revolutionary.Components;
/// <summary>
/// Given to heads at round start for Revs. Used for tracking if heads died or not.
/// Given to heads at round start. Used for assigning traitors to kill heads and for revs to check if the heads died or not.
/// </summary>
[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
[RegisterComponent]
public sealed partial class CommandStaffComponent : Component
{

View File

@@ -12,9 +12,13 @@ using Robust.Shared.Timing;
namespace Content.Server.ServerUpdates;
/// <summary>
/// Responsible for restarting the server for update, when not disruptive.
/// Responsible for restarting the server periodically or for update, when not disruptive.
/// </summary>
public sealed class ServerUpdateManager
/// <remarks>
/// This was originally only designed for restarting on *update*,
/// but now also handles periodic restarting to keep server uptime via <see cref="CCVars.ServerUptimeRestartMinutes"/>.
/// </remarks>
public sealed class ServerUpdateManager : IPostInjectInit
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IWatchdogApi _watchdog = default!;
@@ -22,23 +26,43 @@ public sealed class ServerUpdateManager
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IBaseServer _server = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
[ViewVariables]
private bool _updateOnRoundEnd;
private TimeSpan? _restartTime;
private TimeSpan _uptimeRestart;
public void Initialize()
{
_watchdog.UpdateReceived += WatchdogOnUpdateReceived;
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
_cfg.OnValueChanged(
CCVars.ServerUptimeRestartMinutes,
minutes => _uptimeRestart = TimeSpan.FromMinutes(minutes),
true);
}
public void Update()
{
if (_restartTime != null && _restartTime < _gameTiming.RealTime)
if (_restartTime != null)
{
DoShutdown();
if (_restartTime < _gameTiming.RealTime)
{
DoShutdown();
}
}
else
{
if (ShouldShutdownDueToUptime())
{
ServerEmptyUpdateRestartCheck("uptime");
}
}
}
@@ -48,7 +72,7 @@ public sealed class ServerUpdateManager
/// <returns>True if the server is going to restart.</returns>
public bool RoundEnded()
{
if (_updateOnRoundEnd)
if (_updateOnRoundEnd || ShouldShutdownDueToUptime())
{
DoShutdown();
return true;
@@ -61,11 +85,14 @@ public sealed class ServerUpdateManager
{
switch (e.NewStatus)
{
case SessionStatus.Connecting:
case SessionStatus.Connected:
if (_restartTime != null)
_sawmill.Debug("Aborting server restart timer due to player connection");
_restartTime = null;
break;
case SessionStatus.Disconnected:
ServerEmptyUpdateRestartCheck();
ServerEmptyUpdateRestartCheck("last player disconnect");
break;
}
}
@@ -74,20 +101,20 @@ public sealed class ServerUpdateManager
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received"));
_updateOnRoundEnd = true;
ServerEmptyUpdateRestartCheck();
ServerEmptyUpdateRestartCheck("update notification");
}
/// <summary>
/// Checks whether there are still players on the server,
/// and if not starts a timer to automatically reboot the server if an update is available.
/// </summary>
private void ServerEmptyUpdateRestartCheck()
private void ServerEmptyUpdateRestartCheck(string reason)
{
// Can't simple check the current connected player count since that doesn't update
// before PlayerStatusChanged gets fired.
// So in the disconnect handler we'd still see a single player otherwise.
var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
if (playersOnline || !_updateOnRoundEnd)
if (playersOnline || !(_updateOnRoundEnd || ShouldShutdownDueToUptime()))
{
// Still somebody online.
return;
@@ -95,16 +122,30 @@ public sealed class ServerUpdateManager
if (_restartTime != null)
{
// Do nothing because I guess we already have a timer running..?
// Do nothing because we already have a timer running.
return;
}
var restartDelay = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.UpdateRestartDelay));
_restartTime = restartDelay + _gameTiming.RealTime;
_sawmill.Debug("Started server-empty restart timer due to {Reason}", reason);
}
private void DoShutdown()
{
_server.Shutdown(Loc.GetString("server-updates-shutdown"));
_sawmill.Debug($"Shutting down via {nameof(ServerUpdateManager)}!");
var reason = _updateOnRoundEnd ? "server-updates-shutdown" : "server-updates-shutdown-uptime";
_server.Shutdown(Loc.GetString(reason));
}
private bool ShouldShutdownDueToUptime()
{
return _uptimeRestart != TimeSpan.Zero && _gameTiming.RealTime > _uptimeRestart;
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("restart");
}
}

View File

@@ -0,0 +1,183 @@
using Content.Server.Administration;
using Content.Server.Labels;
using Content.Shared.Administration;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Commands;
/// <summary>
/// Creates FTL disks, to maps, grids, or entities.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class FTLDiskCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IEntitySystemManager _entSystemManager = default!;
public override string Command => "ftldisk";
[ValidatePrototypeId<EntityPrototype>]
public const string CoordinatesDisk = "CoordinatesDisk";
[ValidatePrototypeId<EntityPrototype>]
public const string DiskCase = "DiskCase";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
return;
}
var player = shell.Player;
if (player == null)
{
shell.WriteLine(Loc.GetString("shell-only-players-can-run-this-command"));
return;
}
if (player.AttachedEntity == null)
{
shell.WriteLine(Loc.GetString("shell-must-be-attached-to-entity"));
return;
}
EntityUid entity = player.AttachedEntity.Value;
var coords = _entManager.GetComponent<TransformComponent>(entity).Coordinates;
var handsSystem = _entSystemManager.GetEntitySystem<SharedHandsSystem>();
var labelSystem = _entSystemManager.GetEntitySystem<LabelSystem>();
var mapSystem = _entSystemManager.GetEntitySystem<SharedMapSystem>();
var storageSystem = _entSystemManager.GetEntitySystem<SharedStorageSystem>();
foreach (var destinations in args)
{
DebugTools.AssertNotNull(destinations);
// make sure destination is an id.
EntityUid dest;
if (_entManager.TryParseNetEntity(destinations, out var nullableDest))
{
DebugTools.AssertNotNull(nullableDest);
dest = (EntityUid) nullableDest;
// we need to go to a map, so check if the EntID is something else then try for its map
if (!_entManager.HasComponent<MapComponent>(dest))
{
if (!_entManager.TryGetComponent<TransformComponent>(dest, out var entTransform))
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-transform", ("destination", destinations)));
continue;
}
if (!mapSystem.TryGetMap(entTransform.MapID, out var mapDest))
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map", ("destination", destinations)));
continue;
}
DebugTools.AssertNotNull(mapDest);
dest = mapDest!.Value; // explicit cast here should be fine since the previous if should catch it.
}
// find and verify the map is not somehow unusable.
if (!_entManager.TryGetComponent<MapComponent>(dest, out var mapComp)) // We have to check for a MapComponent here and above since we could have changed our dest entity.
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map-comp", ("destination", destinations), ("map", dest)));
continue;
}
if (mapComp.MapInitialized == false)
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-not-init", ("destination", destinations), ("map", dest)));
continue;
}
if (mapComp.MapPaused == true)
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-paused", ("destination", destinations), ("map", dest)));
continue;
}
// check if our destination works already, if not, make it.
if (!_entManager.TryGetComponent<FTLDestinationComponent>(dest, out var ftlDestComp))
{
FTLDestinationComponent ftlDest = _entManager.AddComponent<FTLDestinationComponent>(dest);
ftlDest.RequireCoordinateDisk = true;
if (_entManager.HasComponent<MapGridComponent>(dest))
{
ftlDest.BeaconsOnly = true;
shell.WriteLine(Loc.GetString("cmd-ftldisk-planet", ("destination", destinations), ("map", dest)));
}
}
else
{
// we don't do these automatically, since it isn't clear what the correct resolution is. Instead we provide feedback to the user and carry on like they know what theyre doing.
if (ftlDestComp.Enabled == false)
shell.WriteLine(Loc.GetString("cmd-ftldisk-already-dest-not-enabled", ("destination", destinations), ("map", dest)));
if (ftlDestComp.BeaconsOnly == true)
shell.WriteLine(Loc.GetString("cmd-ftldisk-requires-ftl-point", ("destination", destinations), ("map", dest)));
}
// create the FTL disk
EntityUid cdUid = _entManager.SpawnEntity(CoordinatesDisk, coords);
var cd = _entManager.EnsureComponent<ShuttleDestinationCoordinatesComponent>(cdUid);
cd.Destination = dest;
_entManager.Dirty(cdUid, cd);
// create disk case
EntityUid cdCaseUid = _entManager.SpawnEntity(DiskCase, coords);
// apply labels
if (_entManager.TryGetComponent<MetaDataComponent>(dest, out var meta) && meta != null && meta.EntityName != null)
{
labelSystem.Label(cdUid, meta.EntityName);
labelSystem.Label(cdCaseUid, meta.EntityName);
}
// if the case has a storage, try to place the disk in there and then the case inhand
if (_entManager.TryGetComponent<StorageComponent>(cdCaseUid, out var storage) && storageSystem.Insert(cdCaseUid, cdUid, out _, storageComp: storage, playSound: false))
{
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
handsSystem.TryPickup(entity, cdCaseUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
else // the case was messed up, put disk inhand
{
_entManager.DeleteEntity(cdCaseUid); // something went wrong so just yeet the chaf
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
handsSystem.TryPickup(entity, cdUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
}
else
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-uid", ("uid", destinations)));
}
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length >= 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapUids(_entManager), Loc.GetString("cmd-ftldisk-hint"));
return CompletionResult.Empty;
}
}

View File

@@ -60,6 +60,10 @@ public sealed partial class BorgSystem
if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
{
if(TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
{
action.Icon = moduleIconComp.Icon;
};
action.EntityIcon = uid;
Dirty(component.ModuleSwapActionEntity.Value, action);
}

View File

@@ -293,6 +293,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
while (query.MoveNext(out var update))
{
SetLaws(lawset, update);
if (provider.LawUploadSound != null && _mind.TryGetMind(update, out var mindId, out _))
_roles.MindPlaySound(mindId, provider.LawUploadSound);
}
}
}

View File

@@ -131,7 +131,7 @@ public sealed class TemperatureSystem : EntitySystem
TemperatureComponent? temperature = null)
{
//CrystallPunk may try place on heater and entity, and solutions
//if (!Resolve(uid, ref temperature))
//if (!Resolve(uid, ref temperature, false))
// return;
if (temperature == null)
return;
@@ -315,7 +315,7 @@ public sealed class TemperatureSystem : EntitySystem
private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
{
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
private void OnParentChange(EntityUid uid, TemperatureComponent component,

View File

@@ -1,4 +1,5 @@
using Content.Server.Traitor.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Components;
@@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components;
public sealed partial class AutoTraitorComponent : Component
{
/// <summary>
/// Whether to give the traitor an uplink or not.
/// The traitor profile to use
/// </summary>
[DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveUplink = true;
/// <summary>
/// Whether to give the traitor objectives or not.
/// </summary>
[DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveObjectives = true;
[DataField]
public EntProtoId Profile = "Traitor";
}

View File

@@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
public override void Initialize()
{
base.Initialize();
@@ -24,6 +21,6 @@ public sealed class AutoTraitorSystem : EntitySystem
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
{
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile);
}
}

View File

@@ -1,97 +1,136 @@
using System.Linq;
using Content.Server.Store.Systems;
using Content.Server.StoreDiscount.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Uplink
namespace Content.Server.Traitor.Uplink;
public sealed class UplinkSystem : EntitySystem
{
public sealed class UplinkSystem : EntitySystem
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
private const string FallbackUplinkImplant = "UplinkImplant";
private const string FallbackUplinkCatalog = "UplinkUplinkImplanter";
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="user">The person who is getting the uplink</param>
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(
EntityUid user,
FixedPoint2 balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false)
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
// Try to find target item if none passed
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
uplinkEntity ??= FindUplinkTarget(user);
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="user">The person who is getting the uplink</param>
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(
EntityUid user,
FixedPoint2? balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false
)
if (uplinkEntity == null)
return ImplantUplink(user, balance, giveDiscounts);
EnsureComp<UplinkComponent>(uplinkEntity.Value);
SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
// TODO add BUI. Currently can't be done outside of yaml -_-
// ^ What does this even mean?
return true;
}
/// <summary>
/// Configure TC for the uplink
/// </summary>
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
{
var store = EnsureComp<StoreComponent>(uplink);
store.AccountOwner = user;
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
uplink,
store);
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplink,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplink, store)
.ToArray());
RaiseLocalEvent(ref uplinkInitializedEvent);
}
/// <summary>
/// Implant an uplink as a fallback measure if the traitor had no PDA
/// </summary>
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
{
var implantProto = new string(FallbackUplinkImplant);
if (!_proto.TryIndex<ListingPrototype>(FallbackUplinkCatalog, out var catalog))
return false;
if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost))
return false;
if (balance < cost) // Can't use Math functions on FixedPoint2
balance = 0;
else
balance = balance - cost;
var implant = _subdermalImplant.AddImplant(user, implantProto);
if (!HasComp<StoreComponent>(implant))
return false;
SetUplink(user, implant.Value, balance, giveDiscounts);
return true;
}
/// <summary>
/// Finds the entity that can hold an uplink for a user.
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
/// </summary>
public EntityUid? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
// Try to find target item if none passed
uplinkEntity ??= FindUplinkTarget(user);
if (uplinkEntity == null)
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
return false;
if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
EnsureComp<UplinkComponent>(uplinkEntity.Value);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
store.AccountOwner = user;
store.Balance.Clear();
if (balance != null)
{
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store);
}
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplinkEntity.Value,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store)
.ToArray()
);
RaiseLocalEvent(ref uplinkInitializedEvent);
// TODO add BUI. Currently can't be done outside of yaml -_-
return true;
}
/// <summary>
/// Finds the entity that can hold an uplink for a user.
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
/// </summary>
public EntityUid? FindUplinkTarget(EntityUid user)
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
}
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
return item;
}
return null;
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
return item;
}
return null;
}
}

View File

@@ -56,8 +56,14 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));
}
protected override bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId,
EntityUid ignore, ICommonSession? session)
protected override bool ArcRaySuccessful(EntityUid targetUid,
Vector2 position,
Angle angle,
Angle arcWidth,
float range,
MapId mapId,
EntityUid ignore,
ICommonSession? session)
{
// Originally the client didn't predict damage effects so you'd intuit some level of how far
// in the future you'd need to predict, but then there was a lot of complaining like "why would you add artifical delay" as if ping is a choice.

View File

@@ -2,6 +2,8 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Mind.Components;
using Content.Shared.Teleportation.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
@@ -11,6 +13,7 @@ public sealed class PortalArtifactSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
@@ -21,21 +24,28 @@ public sealed class PortalArtifactSystem : EntitySystem
private void OnActivate(Entity<PortalArtifactComponent> artifact, ref ArtifactActivatedEvent args)
{
var map = Transform(artifact).MapID;
var validMinds = new ValueList<EntityUid>();
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent, MetaDataComponent>();
while (mindQuery.MoveNext(out var uid, out var mc, out var xform, out var meta))
{
// check if the MindContainer has a Mind and if the entity is not in a container (this also auto excludes AI) and if they are on the same map
if (mc.HasMind && !_container.IsEntityOrParentInContainer(uid, meta: meta, xform: xform) && xform.MapID == map)
{
validMinds.Add(uid);
}
}
//this would only be 0 if there were a station full of AIs and no one else, in that case just stop this function
if (validMinds.Count == 0)
return;
var firstPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(artifact));
var minds = new List<EntityUid>();
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent>();
while (mindQuery.MoveNext(out var uid, out var mc, out var xform))
{
if (mc.HasMind && xform.MapID == map)
minds.Add(uid);
}
var target = _random.Pick(validMinds);
var target = _random.Pick(minds);
var secondPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(target));
//Manual position swapping, because the portal that opens doesn't trigger a collision, and doesn't teleport targets the first time.
_transform.SwapPositions(target, secondPortal);
_transform.SwapPositions(target, artifact.Owner);
_link.TryLink(firstPortal, secondPortal, true);
}

View File

@@ -178,15 +178,13 @@ namespace Content.Shared.APC
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable<ApcBoundInterfaceState>
{
public readonly bool MainBreaker;
public readonly bool HasAccess;
public readonly int Power;
public readonly ApcExternalPowerState ApcExternalPower;
public readonly float Charge;
public ApcBoundInterfaceState(bool mainBreaker, bool hasAccess, int power, ApcExternalPowerState apcExternalPower, float charge)
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge)
{
MainBreaker = mainBreaker;
HasAccess = hasAccess;
Power = power;
ApcExternalPower = apcExternalPower;
Charge = charge;
@@ -197,7 +195,6 @@ namespace Content.Shared.APC
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return MainBreaker == other.MainBreaker &&
HasAccess == other.HasAccess &&
Power == other.Power &&
ApcExternalPower == other.ApcExternalPower &&
MathHelper.CloseTo(Charge, other.Charge);
@@ -210,7 +207,7 @@ namespace Content.Shared.APC
public override int GetHashCode()
{
return HashCode.Combine(MainBreaker, HasAccess, Power, (int) ApcExternalPower, Charge);
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Actions;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting;
using Content.Shared.Examine;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.IdentityManagement;
@@ -61,6 +62,7 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt);
}
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
@@ -310,6 +312,14 @@ public sealed partial class SleepingSystem : EntitySystem
Wake((ent, ent.Comp));
return true;
}
/// <summary>
/// Prevents the use of emote actions while sleeping
/// </summary>
public void OnEmoteAttempt(Entity<SleepingComponent> ent, ref EmoteAttemptEvent args)
{
args.Cancel();
}
}

View File

@@ -45,6 +45,21 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> DefaultGuide =
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// If greater than 0, automatically restart the server after this many minutes of uptime.
/// </summary>
/// <remarks>
/// <para>
/// This is intended to work around various bugs and performance issues caused by long continuous server uptime.
/// </para>
/// <para>
/// This uses the same non-disruptive logic as update restarts,
/// i.e. the game will only restart at round end or when there is nobody connected.
/// </para>
/// </remarks>
public static readonly CVarDef<int> ServerUptimeRestartMinutes =
CVarDef.Create("server.uptime_restart_minutes", 0, CVar.SERVERONLY);
/*
* Ambience
*/
@@ -449,6 +464,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<float> GameEntityMenuLookup =
CVarDef.Create("game.entity_menu_lookup", 0.25f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Should the clients window show the server hostname in the title?
/// </summary>
public static readonly CVarDef<bool> GameHostnameInTitlebar =
CVarDef.Create("game.hostname_in_titlebar", true, CVar.SERVER | CVar.REPLICATED);
/*
* Discord
*/

View File

@@ -84,6 +84,35 @@ public abstract class SharedChatSystem : EntitySystem
return current ?? _prototypeManager.Index<SpeechVerbPrototype>(speech.SpeechVerb);
}
/// <summary>
/// Splits the input message into a radio prefix part and the rest to preserve it during sanitization.
/// </summary>
/// <remarks>
/// This is primarily for the chat emote sanitizer, which can match against ":b" as an emote, which is a valid radio keycode.
/// </remarks>
public void GetRadioKeycodePrefix(EntityUid source,
string input,
out string output,
out string prefix)
{
prefix = string.Empty;
output = input;
// If the string is less than 2, then it's probably supposed to be an emote.
// No one is sending empty radio messages!
if (input.Length <= 2)
return;
if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
return;
if (!_keyCodes.TryGetValue(char.ToLower(input[1]), out _))
return;
prefix = input[..2];
output = input[2..];
}
/// <summary>
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.

View File

@@ -0,0 +1,13 @@
namespace Content.Shared.Chemistry.Events;
/// <summary>
/// Raised directed on an entity when it embeds in another entity.
/// </summary>
[ByRefEvent]
public readonly record struct InjectOverTimeEvent(EntityUid embeddedIntoUid)
{
/// <summary>
/// Entity that is embedded in.
/// </summary>
public readonly EntityUid EmbeddedIntoUid = embeddedIntoUid;
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Ghost;
/// <summary>
/// Marker component to identify "ghostly" entities.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SpectralComponent : Component { }

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory.VirtualItem;
@@ -130,7 +131,7 @@ public abstract partial class SharedHandsSystem
TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
return true;
}
// drop the item with heavy calculations from their hands and place it at the calculated interaction range position
// The DoDrop is handle if there's no drop target
DoDrop(uid, hand, doDropInteraction: doDropInteraction, handsComp);
@@ -138,7 +139,7 @@ public abstract partial class SharedHandsSystem
// if there's no drop location stop here
if (targetDropLocation == null)
return true;
// otherwise, also move dropped item and rotate it properly according to grid/map
var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
var origin = new MapCoordinates(itemPos, itemXform.MapID);
@@ -197,7 +198,7 @@ public abstract partial class SharedHandsSystem
/// <summary>
/// Removes the contents of a hand from its container. Assumes that the removal is allowed. In general, you should not be calling this directly.
/// </summary>
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null)
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null, bool log = true)
{
if (!Resolve(uid, ref handsComp))
return;
@@ -221,6 +222,9 @@ public abstract partial class SharedHandsSystem
if (doDropInteraction)
_interactionSystem.DroppedInteraction(uid, entity);
if (log)
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(uid):user} dropped {ToPrettyString(entity):entity}");
if (hand == handsComp.ActiveHand)
RaiseLocalEvent(entity, new HandDeselectedEvent(uid));
}

View File

@@ -178,8 +178,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
if (!CanPickupToHand(uid, entity, handsComp.ActiveHand, checkActionBlocker, handsComp))
return false;
DoDrop(uid, hand, false, handsComp);
DoPickup(uid, handsComp.ActiveHand, entity, handsComp);
DoDrop(uid, hand, false, handsComp, log:false);
DoPickup(uid, handsComp.ActiveHand, entity, handsComp, log: false);
return true;
}

View File

@@ -220,7 +220,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
/// <summary>
/// Puts an entity into the player's hand, assumes that the insertion is allowed. In general, you should not be calling this function directly.
/// </summary>
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null)
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null, bool log = true)
{
if (!Resolve(uid, ref hands))
return;
@@ -235,7 +235,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
return;
}
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
if (log)
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
Dirty(uid, hands);

View File

@@ -94,22 +94,38 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary>
public void AddImplants(EntityUid uid, IEnumerable<String> implants)
{
var coords = Transform(uid).Coordinates;
foreach (var id in implants)
{
var ent = Spawn(id, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
}
AddImplant(uid, id);
}
}
/// <summary>
/// Adds a single implant to a person, and returns the implant.
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
/// </summary>
/// <returns>
/// The implant, if it was successfully created. Otherwise, null.
/// </returns>>
public EntityUid? AddImplant(EntityUid uid, String implantId)
{
var coords = Transform(uid).Coordinates;
var ent = Spawn(implantId, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
return null;
}
return ent;
}
/// <summary>
/// Forces an implant into a person
/// Good for on spawn related code or admin additions

View File

@@ -8,7 +8,7 @@ namespace Content.Shared.Interaction.Events;
/// </remarks>
public sealed class ContactInteractionEvent : HandledEntityEventArgs
{
public readonly EntityUid Other;
public EntityUid Other;
public ContactInteractionEvent(EntityUid other)
{

View File

@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when failing to pet/hug something.
/// </summary>
// TODO INTERACTION
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
[ByRefEvent]
public readonly record struct InteractionFailureEvent(EntityUid User);

View File

@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when successfully petting/hugging something.
/// </summary>
// TODO INTERACTION
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
[ByRefEvent]
public readonly record struct InteractionSuccessEvent(EntityUid User);

View File

@@ -456,8 +456,22 @@ namespace Content.Shared.Interaction
inRangeUnobstructed);
}
private bool IsDeleted(EntityUid uid)
{
return TerminatingOrDeleted(uid) || EntityManager.IsQueuedForDeletion(uid);
}
private bool IsDeleted(EntityUid? uid)
{
//optional / null entities can pass this validation check. I.e., is-deleted returns false for null uids
return uid != null && IsDeleted(uid.Value);
}
public void InteractHand(EntityUid user, EntityUid target)
{
if (IsDeleted(user) || IsDeleted(target))
return;
var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
if (!complexInteractions)
{
@@ -466,7 +480,8 @@ namespace Content.Shared.Interaction
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false,
complexInteractions: complexInteractions);
complexInteractions: complexInteractions,
checkDeletion: false);
return;
}
@@ -479,6 +494,7 @@ namespace Content.Shared.Interaction
return;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message, true);
@@ -487,18 +503,23 @@ namespace Content.Shared.Interaction
if (message.Handled)
return;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// Else we run Activate.
InteractionActivate(user,
target,
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false,
complexInteractions: complexInteractions);
complexInteractions: complexInteractions,
checkDeletion: false);
}
public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
EntityCoordinates clickLocation, bool inRangeUnobstructed)
{
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
return;
if (target != null)
{
_adminLogger.Add(
@@ -514,9 +535,10 @@ namespace Content.Shared.Interaction
$"{ToPrettyString(user):user} interacted with *nothing* using {ToPrettyString(used):used}");
}
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed))
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false))
return;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
if (target != null)
{
var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
@@ -524,12 +546,12 @@ namespace Content.Shared.Interaction
// We contact the USED entity, but not the target.
DoContactInteraction(user, used, rangedMsg);
if (rangedMsg.Handled)
return;
}
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed);
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false);
}
protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates)
@@ -933,11 +955,18 @@ namespace Content.Shared.Interaction
EntityUid used,
EntityUid? target,
EntityCoordinates clickLocation,
bool canReach)
bool canReach,
bool checkDeletion = true)
{
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
return false;
var ev = new BeforeRangedInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used, ev);
if (!ev.Handled)
return false;
// We contact the USED entity, but not the target.
DoContactInteraction(user, used, ev);
return ev.Handled;
@@ -966,6 +995,9 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkCanUse = true)
{
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
return false;
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
return false;
@@ -977,9 +1009,10 @@ namespace Content.Shared.Interaction
LogImpact.Low,
$"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}");
if (RangedInteractDoBefore(user, used, target, clickLocation, true))
if (RangedInteractDoBefore(user, used, target, clickLocation, canReach: true, checkDeletion: false))
return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
// all interactions should only happen when in range / unobstructed, so no range check is needed
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target, interactUsingEvent, true);
@@ -989,8 +1022,10 @@ namespace Content.Shared.Interaction
if (interactUsingEvent.Handled)
return true;
if (InteractDoAfter(user, used, target, clickLocation, canReach: true))
if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false))
return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
return false;
}
@@ -1004,11 +1039,14 @@ namespace Content.Shared.Interaction
/// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.
/// </param>
/// <returns>True if the interaction was handled. Otherwise, false.</returns>
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach)
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach, bool checkDeletion = true)
{
if (target is { Valid: false })
target = null;
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
return false;
var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used, afterInteractEvent);
DoContactInteraction(user, used, afterInteractEvent);
@@ -1024,6 +1062,7 @@ namespace Content.Shared.Interaction
if (target == null)
return false;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
var afterInteractUsingEvent = new AfterInteractUsingEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(target.Value, afterInteractUsingEvent);
@@ -1034,9 +1073,7 @@ namespace Content.Shared.Interaction
// Contact interactions are currently only used for forensics, so we don't raise used -> target
}
if (afterInteractUsingEvent.Handled)
return true;
return false;
return afterInteractUsingEvent.Handled;
}
#region ActivateItemInWorld
@@ -1068,8 +1105,13 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkUseDelay = true,
bool checkAccess = true,
bool? complexInteractions = null)
bool? complexInteractions = null,
bool checkDeletion = true)
{
if (checkDeletion && (IsDeleted(user) || IsDeleted(used)))
return false;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
_delayQuery.TryComp(used, out var delayComponent);
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
return false;
@@ -1085,21 +1127,32 @@ namespace Content.Shared.Interaction
if (checkAccess && !IsAccessible(user, used))
return false;
complexInteractions ??= SupportsComplexInteractions(user);
complexInteractions ??= _actionBlockerSystem.CanComplexInteract(user);
var activateMsg = new ActivateInWorldEvent(user, used, complexInteractions.Value);
RaiseLocalEvent(used, activateMsg, true);
if (activateMsg.Handled)
{
DoContactInteraction(user, used);
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
if (delayComponent != null)
_useDelay.TryResetDelay(used, component: delayComponent);
return true;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
var userEv = new UserActivateInWorldEvent(user, used, complexInteractions.Value);
RaiseLocalEvent(user, userEv, true);
if (!activateMsg.Handled && !userEv.Handled)
if (!userEv.Handled)
return false;
DoContactInteraction(user, used, activateMsg);
DoContactInteraction(user, used);
// Still need to call this even without checkUseDelay in case this gets relayed from Activate.
if (delayComponent != null)
_useDelay.TryResetDelay(used, component: delayComponent);
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
return true;
}
#endregion
@@ -1118,6 +1171,9 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkUseDelay = true)
{
if (IsDeleted(user) || IsDeleted(used))
return false;
_delayQuery.TryComp(used, out var delayComponent);
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
return true; // if the item is on cooldown, we consider this handled.
@@ -1138,8 +1194,9 @@ namespace Content.Shared.Interaction
return true;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
// else, default to activating the item
return InteractionActivate(user, used, false, false, false);
return InteractionActivate(user, used, false, false, false, checkDeletion: false);
}
/// <summary>
@@ -1164,10 +1221,11 @@ namespace Content.Shared.Interaction
public void DroppedInteraction(EntityUid user, EntityUid item)
{
if (IsDeleted(user) || IsDeleted(item))
return;
var dropMsg = new DroppedEvent(user);
RaiseLocalEvent(item, dropMsg, true);
if (dropMsg.Handled)
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}");
// If the dropper is rotated then use their targetrelativerotation as the drop rotation
var rotation = Angle.Zero;
@@ -1314,15 +1372,21 @@ namespace Content.Shared.Interaction
if (uidB == null || args?.Handled == false)
return;
// Entities may no longer exist (banana was eaten, or human was exploded)?
if (!Exists(uidA) || !Exists(uidB))
if (uidA == uidB.Value)
return;
if (Paused(uidA) || Paused(uidB.Value))
if (!TryComp(uidA, out MetaDataComponent? metaA) || metaA.EntityPaused)
return;
RaiseLocalEvent(uidA, new ContactInteractionEvent(uidB.Value));
RaiseLocalEvent(uidB.Value, new ContactInteractionEvent(uidA));
if (!TryComp(uidB, out MetaDataComponent? metaB) || metaB.EntityPaused)
return ;
// TODO Struct event
var ev = new ContactInteractionEvent(uidB.Value);
RaiseLocalEvent(uidA, ev);
ev.Other = uidA;
RaiseLocalEvent(uidB.Value, ev);
}

View File

@@ -41,6 +41,8 @@ namespace Content.Shared.Localizations
_loc.AddFunction(culture, "LOC", FormatLoc);
_loc.AddFunction(culture, "NATURALFIXED", FormatNaturalFixed);
_loc.AddFunction(culture, "NATURALPERCENT", FormatNaturalPercent);
_loc.AddFunction(culture, "PLAYTIME", FormatPlaytime);
_loc.AddFunction(culture, "MANY", FormatMany); // TODO: Temporary fix for MANY() fluent errors. Remove after resolve errors.
/*
@@ -151,6 +153,16 @@ namespace Content.Shared.Localizations
return Loc.GetString($"zzzz-fmt-direction-{dir.ToString()}");
}
/// <summary>
/// Formats playtime as hours and minutes.
/// </summary>
public static string FormatPlaytime(TimeSpan time)
{
var hours = (int)time.TotalHours;
var minutes = time.Minutes;
return Loc.GetString($"zzzz-fmt-playtime", ("hours", hours), ("minutes", minutes));
}
private static ILocValue FormatLoc(LocArgs args)
{
var id = ((LocValueString) args.Args[0]).Value;
@@ -239,5 +251,15 @@ namespace Content.Shared.Localizations
return new LocValueString(res);
}
private static ILocValue FormatPlaytime(LocArgs args)
{
var time = TimeSpan.Zero;
if (args.Args is { Count: > 0 } && args.Args[0].Value is TimeSpan timeArg)
{
time = timeArg;
}
return new LocValueString(FormatPlaytime(time));
}
}
}

View File

@@ -483,19 +483,6 @@ public abstract class SharedMindSystem : EntitySystem
return false;
}
/// <summary>
/// Gets a role component from a player's mind.
/// </summary>
/// <returns>Whether a role was found</returns>
public bool TryGetRole<T>(EntityUid user, [NotNullWhen(true)] out T? role) where T : IComponent
{
role = default;
if (!TryComp<MindContainerComponent>(user, out var mindContainer) || mindContainer.Mind == null)
return false;
return TryComp(mindContainer.Mind, out role);
}
/// <summary>
/// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the
/// entity that any mind is connected to, except as a side effect of the fact that it may change a player's

View File

@@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component
/// </summary>
[ViewVariables]
public Dictionary<NetEntity, SharedNavMapSystem.NavMapBeacon> Beacons = new();
/// <summary>
/// Describes the properties of a region on the station.
/// It is indexed by the entity assigned as the region owner.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, SharedNavMapSystem.NavMapRegionProperties> RegionProperties = new();
/// <summary>
/// All flood filled regions, ready for display on a NavMapControl.
/// It is indexed by the entity assigned as the region owner.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, NavMapRegionOverlay> RegionOverlays = new();
/// <summary>
/// A queue of all region owners that are waiting their associated regions to be floodfilled.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Queue<NetEntity> QueuedRegionsToFlood = new();
/// <summary>
/// A look up table to get a list of region owners associated with a flood filled chunk.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<Vector2i, HashSet<NetEntity>> ChunkToRegionOwnerTable = new();
/// <summary>
/// A look up table to find flood filled chunks associated with a given region owner.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, HashSet<Vector2i>> RegionOwnerToChunkTable = new();
}
[Serializable, NetSerializable]
@@ -51,10 +95,30 @@ public sealed class NavMapChunk(Vector2i origin)
public GameTick LastUpdate;
}
[Serializable, NetSerializable]
public sealed class NavMapRegionOverlay(Enum uiKey, List<(Vector2i, Vector2i)> gridCoords)
{
/// <summary>
/// The key to the UI that will be displaying this region on its navmap
/// </summary>
public Enum UiKey = uiKey;
/// <summary>
/// The local grid coordinates of the rectangles that make up the region
/// Item1 is the top left corner, Item2 is the bottom right corner
/// </summary>
public List<(Vector2i, Vector2i)> GridCoords = gridCoords;
/// <summary>
/// Color of the region
/// </summary>
public Color Color = Color.White;
}
public enum NavMapChunkType : byte
{
// Values represent bit shift offsets when retrieving data in the tile array.
Invalid = byte.MaxValue,
Invalid = byte.MaxValue,
Floor = 0, // I believe floors have directional information for diagonal tiles?
Wall = SharedNavMapSystem.Directions,
Airlock = 2 * SharedNavMapSystem.Directions,

View File

@@ -3,10 +3,9 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Pinpointer;
@@ -16,7 +15,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public const int Directions = 4; // Not directly tied to number of atmos directions
public const int ChunkSize = 8;
public const int ArraySize = ChunkSize* ChunkSize;
public const int ArraySize = ChunkSize * ChunkSize;
public const int AllDirMask = (1 << Directions) - 1;
public const int AirlockMask = AllDirMask << (int) NavMapChunkType.Airlock;
@@ -24,6 +23,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
[Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!;
[Robust.Shared.IoC.Dependency] private readonly INetManager _net = default!;
private static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
private EntityQuery<NavMapDoorComponent> _doorQuery;
@@ -57,7 +57,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public NavMapChunkType GetEntityType(EntityUid uid)
{
if (_doorQuery.HasComp(uid))
return NavMapChunkType.Airlock;
return NavMapChunkType.Airlock;
if (_tagSystem.HasAnyTag(uid, WallTags))
return NavMapChunkType.Wall;
@@ -81,6 +81,57 @@ public abstract class SharedNavMapSystem : EntitySystem
return true;
}
public void AddOrUpdateNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner, NavMapRegionProperties regionProperties)
{
// Check if a new region has been added or an existing one has been altered
var isDirty = !component.RegionProperties.TryGetValue(regionOwner, out var oldProperties) || oldProperties != regionProperties;
if (isDirty)
{
component.RegionProperties[regionOwner] = regionProperties;
if (_net.IsServer)
Dirty(uid, component);
}
}
public void RemoveNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner)
{
bool regionOwnerRemoved = component.RegionProperties.Remove(regionOwner) | component.RegionOverlays.Remove(regionOwner);
if (regionOwnerRemoved)
{
if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var affectedChunks))
{
foreach (var affectedChunk in affectedChunks)
{
if (component.ChunkToRegionOwnerTable.TryGetValue(affectedChunk, out var regionOwners))
regionOwners.Remove(regionOwner);
}
component.RegionOwnerToChunkTable.Remove(regionOwner);
}
if (_net.IsServer)
Dirty(uid, component);
}
}
public Dictionary<NetEntity, NavMapRegionOverlay> GetNavMapRegionOverlays(EntityUid uid, NavMapComponent component, Enum uiKey)
{
var regionOverlays = new Dictionary<NetEntity, NavMapRegionOverlay>();
foreach (var (regionOwner, regionOverlay) in component.RegionOverlays)
{
if (!regionOverlay.UiKey.Equals(uiKey))
continue;
regionOverlays.Add(regionOwner, regionOverlay);
}
return regionOverlays;
}
#region: Event handling
private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
@@ -97,7 +148,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData);
}
args.State = new NavMapState(chunks, component.Beacons);
args.State = new NavMapState(chunks, component.Beacons, component.RegionProperties);
return;
}
@@ -110,7 +161,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData);
}
args.State = new NavMapDeltaState(chunks, component.Beacons, new(component.Chunks.Keys));
args.State = new NavMapDeltaState(chunks, component.Beacons, component.RegionProperties, new(component.Chunks.Keys));
}
#endregion
@@ -120,22 +171,26 @@ public abstract class SharedNavMapSystem : EntitySystem
[Serializable, NetSerializable]
protected sealed class NavMapState(
Dictionary<Vector2i, int[]> chunks,
Dictionary<NetEntity, NavMapBeacon> beacons)
Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions)
: ComponentState
{
public Dictionary<Vector2i, int[]> Chunks = chunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
}
[Serializable, NetSerializable]
protected sealed class NavMapDeltaState(
Dictionary<Vector2i, int[]> modifiedChunks,
Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions,
HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<NavMapState>
{
public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
public HashSet<Vector2i> AllChunks = allChunks;
public void ApplyToFullState(NavMapState state)
@@ -159,11 +214,18 @@ public abstract class SharedNavMapSystem : EntitySystem
{
state.Beacons.Add(nuid, beacon);
}
state.Regions.Clear();
foreach (var (nuid, region) in Regions)
{
state.Regions.Add(nuid, region);
}
}
public NavMapState CreateNewFullState(NavMapState state)
{
var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count);
foreach (var (index, data) in state.Chunks)
{
if (!AllChunks!.Contains(index))
@@ -177,12 +239,25 @@ public abstract class SharedNavMapSystem : EntitySystem
Array.Copy(newData, data, ArraySize);
}
return new NavMapState(chunks, new(Beacons));
return new NavMapState(chunks, new(Beacons), new(Regions));
}
}
[Serializable, NetSerializable]
public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position);
[Serializable, NetSerializable]
public record struct NavMapRegionProperties(NetEntity Owner, Enum UiKey, HashSet<Vector2i> Seeds)
{
// Server defined color for the region
public Color Color = Color.White;
// The maximum number of tiles that can be assigned to this region
public int MaxArea = 625;
// The maximum distance this region can propagate from its seeds
public int MaxRadius = 25;
}
#endregion
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Power.Generator;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveGeneratorRevvingComponent: Component
public sealed partial class ActiveGeneratorRevvingComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public TimeSpan CurrentTime = TimeSpan.Zero;

View File

@@ -1,6 +1,6 @@
namespace Content.Shared.Power.Generator;
public sealed class ActiveGeneratorRevvingSystem: EntitySystem
public sealed class ActiveGeneratorRevvingSystem : EntitySystem
{
public override void Initialize()
{
@@ -25,7 +25,7 @@ public sealed class ActiveGeneratorRevvingSystem: EntitySystem
/// <param name="component">ActiveGeneratorRevvingComponent of the generator entity.</param>
public void StartAutoRevving(EntityUid uid, ActiveGeneratorRevvingComponent? component = null)
{
if (Resolve(uid, ref component))
if (Resolve(uid, ref component, false))
{
// reset the revving
component.CurrentTime = TimeSpan.FromSeconds(0);

View File

@@ -13,37 +13,43 @@ public sealed partial class EmbeddableProjectileComponent : Component
/// <summary>
/// Minimum speed of the projectile to embed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public float MinimumSpeed = 5f;
/// <summary>
/// Delete the entity on embedded removal?
/// Does nothing if there's no RemovalTime.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool DeleteOnRemove;
/// <summary>
/// How long it takes to remove the embedded object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public float? RemovalTime = 3f;
/// <summary>
/// Whether this entity will embed when thrown, or only when shot as a projectile.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool EmbedOnThrow = true;
/// <summary>
/// How far into the entity should we offset (0 is wherever we collided).
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public Vector2 Offset = Vector2.Zero;
/// <summary>
/// Sound to play after embedding into a hit target.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public SoundSpecifier? Sound;
/// <summary>
/// Uid of the entity the projectile is embed into.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? EmbeddedIntoUid;
}

View File

@@ -71,6 +71,8 @@ public abstract partial class SharedProjectileSystem : EntitySystem
TryComp<PhysicsComponent>(uid, out var physics);
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
_transform.AttachToGridOrMap(uid, xform);
component.EmbeddedIntoUid = null;
Dirty(uid, component);
// Reset whether the projectile has damaged anything if it successfully was removed
if (TryComp<ProjectileComponent>(uid, out var projectile))
@@ -127,8 +129,10 @@ public abstract partial class SharedProjectileSystem : EntitySystem
}
_audio.PlayPredicted(component.Sound, uid, null);
component.EmbeddedIntoUid = target;
var ev = new EmbedEvent(user, target);
RaiseLocalEvent(uid, ref ev);
Dirty(uid, component);
}
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)

View File

@@ -42,10 +42,12 @@ public abstract class SharedJammerSystem : EntitySystem
{
entity.Comp.SelectedPowerLevel = currIndex;
Dirty(entity);
if (_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity)))
{
Popup.PopupPredicted(Loc.GetString(setting.Message), user, user);
}
// If the jammer is off, this won't do anything which is fine.
// The range should be updated when it turns on again!
_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity));
Popup.PopupClient(Loc.GetString(setting.Message), user, user);
},
Text = Loc.GetString(setting.Name),
};

View File

@@ -30,7 +30,7 @@ public sealed partial class AgeRequirement : JobRequirement
if (!Inverted)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-young",
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-young",
("age", RequiredAge)));
if (profile.Age < RequiredAge)
@@ -38,7 +38,7 @@ public sealed partial class AgeRequirement : JobRequirement
}
else
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-old",
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-old",
("age", RequiredAge)));
if (profile.Age > RequiredAge)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
@@ -15,7 +16,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
/// Which department needs the required amount of time.
/// </summary>
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department = default!;
public ProtoId<DepartmentPrototype> Department;
/// <summary>
/// How long (in seconds) this requirement is.
@@ -47,7 +48,9 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
playtime += otherTime;
}
var deptDiff = Time.TotalMinutes - playtime.TotalMinutes;
var deptDiffSpan = Time - playtime;
var deptDiff = deptDiffSpan.TotalMinutes;
var formattedDeptDiff = ContentLocalizationManager.FormatPlaytime(deptDiffSpan);
var nameDepartment = "role-timer-department-unknown";
if (protoManager.TryIndex(Department, out var departmentIndexed))
@@ -62,7 +65,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-insufficient",
("time", Math.Ceiling(deptDiff)),
("time", formattedDeptDiff),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;
@@ -72,7 +75,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-too-high",
("time", -deptDiff),
("time", formattedDeptDiff),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using JetBrains.Annotations;
@@ -25,7 +26,9 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
reason = new FormattedMessage();
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
var overallDiff = Time.TotalMinutes - overallTime.TotalMinutes;
var overallDiffSpan = Time - overallTime;
var overallDiff = overallDiffSpan.TotalMinutes;
var formattedOverallDiff = ContentLocalizationManager.FormatPlaytime(overallDiffSpan);
if (!Inverted)
{
@@ -34,14 +37,14 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
("time", formattedOverallDiff)));
return false;
}
if (overallDiff <= 0 || overallTime >= Time)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high",
("time", -overallDiff)));
("time", formattedOverallDiff)));
return false;
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Content.Shared.Roles.Jobs;
@@ -17,7 +18,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
/// What particular role they need the time requirement with.
/// </summary>
[DataField(required: true)]
public ProtoId<PlayTimeTrackerPrototype> Role = default!;
public ProtoId<PlayTimeTrackerPrototype> Role;
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField(required: true)]
@@ -34,7 +35,9 @@ public sealed partial class RoleTimeRequirement : JobRequirement
string proto = Role;
playTimes.TryGetValue(proto, out var roleTime);
var roleDiff = Time.TotalMinutes - roleTime.TotalMinutes;
var roleDiffSpan = Time - roleTime;
var roleDiff = roleDiffSpan.TotalMinutes;
var formattedRoleDiff = ContentLocalizationManager.FormatPlaytime(roleDiffSpan);
var departmentColor = Color.Yellow;
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
@@ -52,7 +55,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", Math.Ceiling(roleDiff)),
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
@@ -62,7 +65,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", -roleDiff),
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;

View File

@@ -103,7 +103,6 @@ public abstract class SharedJobSystem : EntitySystem
public bool MindHasJobWithId(EntityUid? mindId, string prototypeId)
{
MindRoleComponent? comp = null;
if (mindId is null)
return false;
@@ -112,9 +111,7 @@ public abstract class SharedJobSystem : EntitySystem
if (role is null)
return false;
comp = role.Value.Comp;
return (comp.JobPrototype == prototypeId);
return role.Value.Comp1.JobPrototype == prototypeId;
}
public bool MindTryGetJob(
@@ -124,7 +121,7 @@ public abstract class SharedJobSystem : EntitySystem
prototype = null;
MindTryGetJobId(mindId, out var protoId);
return (_prototypes.TryIndex<JobPrototype>(protoId, out prototype) || prototype is not null);
return _prototypes.TryIndex(protoId, out prototype) || prototype is not null;
}
public bool MindTryGetJobId(
@@ -137,9 +134,9 @@ public abstract class SharedJobSystem : EntitySystem
return false;
if (_roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role))
job = role.Value.Comp.JobPrototype;
job = role.Value.Comp1.JobPrototype;
return (job is not null);
return job is not null;
}
/// <summary>

View File

@@ -42,6 +42,8 @@ public sealed partial class MindRoleComponent : BaseMindRoleComponent
public ProtoId<JobPrototype>? JobPrototype { get; set; }
}
// Why does this base component actually exist? It does make auto-categorization easy, but before that it was useless?
[EntityCategory("Roles")]
public abstract partial class BaseMindRoleComponent : Component
{

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
@@ -92,19 +93,18 @@ public abstract class SharedRoleSystem : EntitySystem
bool silent = false,
string? jobPrototype = null)
{
// Can't have someone get paid for two jobs now, can we
if (MindHasRole<JobRoleComponent>(mindId, out var jobRole)
&& jobRole.Value.Comp.JobPrototype != jobPrototype)
{
Resolve(mindId, ref mind);
if (mind is not null)
{
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp.JobPrototype}' to '{jobPrototype}'");
}
if (!Resolve(mindId, ref mind))
return;
jobRole.Value.Comp.JobPrototype = jobPrototype;
// Can't have someone get paid for two jobs now, can we
if (MindHasRole<JobRoleComponent>((mindId, mind), out var jobRole)
&& jobRole.Value.Comp1.JobPrototype != jobPrototype)
{
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp1.JobPrototype}' to '{jobPrototype}'");
jobRole.Value.Comp1.JobPrototype = jobPrototype;
}
else
MindAddRoleDo(mindId, "MindRoleJob", mind, silent, jobPrototype);
@@ -146,11 +146,12 @@ public abstract class SharedRoleSystem : EntitySystem
{
mindRoleComp.JobPrototype = jobPrototype;
EnsureComp<JobRoleComponent>(mindRoleId);
DebugTools.AssertNull(mindRoleComp.AntagPrototype);
DebugTools.Assert(!mindRoleComp.Antag);
DebugTools.Assert(!mindRoleComp.ExclusiveAntag);
}
if (mindRoleComp.Antag || mindRoleComp.ExclusiveAntag)
antagonist = true;
antagonist |= mindRoleComp.Antag;
mind.MindRoles.Add(mindRoleId);
var mindEv = new MindRoleAddedEvent(silent);
@@ -182,51 +183,55 @@ public abstract class SharedRoleSystem : EntitySystem
/// <summary>
/// Removes all instances of a specific role from this mind.
/// </summary>
/// <param name="mindId">The mind to remove the role from.</param>
/// <param name="mind">The mind to remove the role from.</param>
/// <typeparam name="T">The type of the role to remove.</typeparam>
/// <exception cref="ArgumentException">Thrown if the mind does not exist or does not have this role.</exception>
/// <returns>Returns False if there was something wrong with the mind or the removal. True if successful</returns>>
public bool MindRemoveRole<T>(EntityUid mindId) where T : IComponent
/// <returns>Returns false if the role did not exist. True if successful</returns>>
public bool MindRemoveRole<T>(Entity<MindComponent?> mind) where T : IComponent
{
if (!TryComp<MindComponent>(mindId, out var mind) )
throw new ArgumentException($"{mindId} does not exist or does not have mind component");
if (typeof(T) == typeof(MindRoleComponent))
throw new InvalidOperationException();
if (!Resolve(mind.Owner, ref mind.Comp))
return false;
var found = false;
var antagonist = false;
var delete = new List<EntityUid>();
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
if (!HasComp<T>(role))
continue;
var roleComp = Comp<MindRoleComponent>(role);
antagonist = roleComp.Antag;
_entityManager.DeleteEntity(role);
if (!TryComp(role, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
continue;
}
antagonist |= roleComp.Antag | roleComp.ExclusiveAntag;
_entityManager.DeleteEntity(role);
delete.Add(role);
found = true;
}
foreach (var role in delete)
{
mind.MindRoles.Remove(role);
}
if (!found)
return false;
foreach (var role in delete)
{
throw new ArgumentException($"{mindId} does not have this role: {typeof(T)}");
mind.Comp.MindRoles.Remove(role);
}
var message = new RoleRemovedEvent(mindId, mind, antagonist);
if (mind.OwnedEntity != null)
if (mind.Comp.OwnedEntity != null)
{
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
var message = new RoleRemovedEvent(mind.Owner, mind.Comp, antagonist);
RaiseLocalEvent(mind.Comp.OwnedEntity.Value, message, true);
}
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"'Role {typeof(T).Name}' removed from mind of {ToPrettyString(mind.OwnedEntity)}");
$"All roles of type '{typeof(T).Name}' removed from mind of {ToPrettyString(mind.Comp.OwnedEntity)}");
return true;
}
@@ -238,16 +243,14 @@ public abstract class SharedRoleSystem : EntitySystem
/// <returns>True if the role existed and was removed</returns>
public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
{
if (!MindHasRole<T>(mindId))
{
Log.Warning($"Failed to remove role {typeof(T)} from {mindId} : mind does not have role ");
return false;
}
if (typeof(T) == typeof(MindRoleComponent))
return false;
return MindRemoveRole<T>(mindId);
if (MindRemoveRole<T>(mindId))
return true;
Log.Warning($"Failed to remove role {typeof(T)} from {ToPrettyString(mindId)} : mind does not have role ");
return false;
}
/// <summary>
@@ -259,30 +262,29 @@ public abstract class SharedRoleSystem : EntitySystem
/// <param name="role">The Mind Role entity component</param>
/// <param name="roleT">The Mind Role's entity component for T</param>
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId,
[NotNullWhen(true)] out Entity<MindRoleComponent>? role,
[NotNullWhen(true)] out Entity<T>? roleT) where T : IComponent
public bool MindHasRole<T>(Entity<MindComponent?> mind,
[NotNullWhen(true)] out Entity<MindRoleComponent, T>? role) where T : IComponent
{
role = null;
roleT = null;
if (!TryComp<MindComponent>(mindId, out var mind))
if (!Resolve(mind.Owner, ref mind.Comp))
return false;
var found = false;
foreach (var roleEnt in mind.MindRoles)
foreach (var roleEnt in mind.Comp.MindRoles)
{
if (!HasComp<T>(roleEnt))
if (!TryComp(roleEnt, out T? tcomp))
continue;
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
roleT = (roleEnt,Comp<T>(roleEnt));
found = true;
break;
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
continue;
}
role = (roleEnt, roleComp, tcomp);
return true;
}
return found;
return false;
}
/// <summary>
@@ -317,7 +319,13 @@ public abstract class SharedRoleSystem : EntitySystem
if (!HasComp(roleEnt, type))
continue;
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
continue;
}
role = (roleEnt, roleComp);
found = true;
break;
}
@@ -325,20 +333,6 @@ public abstract class SharedRoleSystem : EntitySystem
return found;
}
/// <summary>
/// Finds the first mind role of a specific type on a mind entity.
/// Outputs an entity component for the mind role's MindRoleComponent
/// </summary>
/// <param name="mindId">The mind entity</param>
/// <param name="role">The Mind Role entity component</param>
/// <typeparam name="T">The type of the role to find.</typeparam>
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId,
[NotNullWhen(true)] out Entity<MindRoleComponent>? role) where T : IComponent
{
return MindHasRole<T>(mindId, out role, out _);
}
/// <summary>
/// Finds the first mind role of a specific type on a mind entity.
/// </summary>
@@ -347,7 +341,7 @@ public abstract class SharedRoleSystem : EntitySystem
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId) where T : IComponent
{
return MindHasRole<T>(mindId, out _, out _);
return MindHasRole<T>(mindId, out _);
}
//TODO: Delete this later
@@ -374,28 +368,31 @@ public abstract class SharedRoleSystem : EntitySystem
/// <summary>
/// Reads all Roles of a mind Entity and returns their data as RoleInfo
/// </summary>
/// <param name="mindId">The mind entity</param>
/// <param name="mind">The mind entity</param>
/// <returns>RoleInfo list</returns>
public List<RoleInfo> MindGetAllRoleInfo(EntityUid mindId)
public List<RoleInfo> MindGetAllRoleInfo(Entity<MindComponent?> mind)
{
var roleInfo = new List<RoleInfo>();
if (!TryComp<MindComponent>(mindId, out var mind))
if (!Resolve(mind.Owner, ref mind.Comp))
return roleInfo;
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
var valid = false;
var name = "game-ticker-unknown-role";
var prototype = "";
string? playTimeTracker = null;
string? playTimeTracker = null;
var comp = Comp<MindRoleComponent>(role);
if (comp.AntagPrototype is not null)
if (!TryComp(role, out MindRoleComponent? comp))
{
prototype = comp.AntagPrototype;
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
continue;
}
if (comp.AntagPrototype is not null)
prototype = comp.AntagPrototype;
if (comp.JobPrototype is not null && comp.AntagPrototype is null)
{
prototype = comp.JobPrototype;
@@ -429,7 +426,7 @@ public abstract class SharedRoleSystem : EntitySystem
}
if (valid)
roleInfo.Add(new RoleInfo(name, comp.Antag || comp.ExclusiveAntag , playTimeTracker, prototype));
roleInfo.Add(new RoleInfo(name, comp.Antag, playTimeTracker, prototype));
}
return roleInfo;
}
@@ -442,12 +439,9 @@ public abstract class SharedRoleSystem : EntitySystem
public bool MindIsAntagonist(EntityUid? mindId)
{
if (mindId is null)
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
return false;
}
return CheckAntagonistStatus(mindId.Value).Item1;
return CheckAntagonistStatus(mindId.Value).Antag;
}
/// <summary>
@@ -458,37 +452,28 @@ public abstract class SharedRoleSystem : EntitySystem
public bool MindIsExclusiveAntagonist(EntityUid? mindId)
{
if (mindId is null)
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
return false;
}
return CheckAntagonistStatus(mindId.Value).Item2;
return CheckAntagonistStatus(mindId.Value).ExclusiveAntag;
}
private (bool, bool) CheckAntagonistStatus(EntityUid mindId)
public (bool Antag, bool ExclusiveAntag) CheckAntagonistStatus(Entity<MindComponent?> mind)
{
if (!TryComp<MindComponent>(mindId, out var mind))
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind component not found");
if (!Resolve(mind.Owner, ref mind.Comp))
return (false, false);
}
var antagonist = false;
var exclusiveAntag = false;
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
if (!TryComp<MindRoleComponent>(role, out var roleComp))
{
//If this ever shows up outside of an integration test, then we need to look into this further.
Log.Warning($"Mind Role Entity {role} does not have MindRoleComponent!");
Log.Error($"Mind Role Entity {ToPrettyString(role)} does not have a MindRoleComponent, despite being listed as a role belonging to {ToPrettyString(mind)}|");
continue;
}
if (roleComp.Antag || exclusiveAntag)
antagonist = true;
if (roleComp.ExclusiveAntag)
exclusiveAntag = true;
antagonist |= roleComp.Antag;
exclusiveAntag |= roleComp.ExclusiveAntag;
}
return (antagonist, exclusiveAntag);
@@ -504,6 +489,9 @@ public abstract class SharedRoleSystem : EntitySystem
_audio.PlayGlobal(sound, mind.Session);
}
// TODO ROLES Change to readonly.
// Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally
// mutated.
public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
@@ -512,6 +500,7 @@ public abstract class SharedRoleSystem : EntitySystem
return job.Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req))
@@ -520,6 +509,7 @@ public abstract class SharedRoleSystem : EntitySystem
return _prototypes.Index(job).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
@@ -528,6 +518,7 @@ public abstract class SharedRoleSystem : EntitySystem
return _prototypes.Index(antag).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))

View File

@@ -0,0 +1,20 @@
//using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.Borgs.Components;
/// <summary>
/// This is used to override the action icon for cyborg actions.
/// Without this component the no-action state will be used.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class BorgModuleIconComponent : Component
{
/// <summary>
/// The action icon for this module
/// </summary>
[DataField]
public SpriteSpecifier.Rsi Icon = default!;
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Audio;
namespace Content.Shared.Silicons.Laws.Components;
@@ -20,4 +21,12 @@ public sealed partial class SiliconLawProviderComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SiliconLawset? Lawset;
/// <summary>
/// The sound that plays for the Silicon player
/// when the particular lawboard has been inserted.
/// </summary>
[DataField]
public SoundSpecifier? LawUploadSound = new SoundPathSpecifier("/Audio/Misc/cryo_warning.ogg");
}

View File

@@ -285,6 +285,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private bool SetupEye(Entity<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return false;
if (ent.Comp.RemoteEntity != null)
return false;
@@ -299,8 +301,11 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void ClearEye(Entity<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return;
QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null;
Dirty(ent);
}
private void AttachEye(Entity<StationAiCoreComponent> ent)
@@ -330,6 +335,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
if (_timing.ApplyingState)
return;
SetupEye(ent);
// Just so text and the likes works properly
_metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
@@ -351,6 +358,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
{
_eye.SetTarget(args.Entity, null, eyeComp);
}
ClearEye(ent);
}
private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)

View File

@@ -1,3 +1,4 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Sound.Components;
@@ -8,4 +9,9 @@ namespace Content.Shared.Sound.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnUIOpenComponent : BaseEmitSoundComponent
{
/// <summary>
/// Blacklist for making the sound not play if certain entities open the UI
/// </summary>
[DataField]
public EntityWhitelist Blacklist = new();
}

View File

@@ -58,7 +58,10 @@ public abstract class SharedEmitSoundSystem : EntitySystem
private void HandleEmitSoundOnUIOpen(EntityUid uid, EmitSoundOnUIOpenComponent component, AfterActivatableUIOpenEvent args)
{
TryEmitSound(uid, component, args.User);
if (_whitelistSystem.IsBlacklistFail(component.Blacklist, args.User))
{
TryEmitSound(uid, component, args.User);
}
}
private void OnMobState(Entity<SoundWhileAliveComponent> entity, ref MobStateChangedEvent args)

View File

@@ -150,6 +150,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
foreach (var (slot, entProtos) in startingGear.Storage)
{
ents.Clear();
if (entProtos.Count == 0)
continue;

View File

@@ -14,6 +14,8 @@ using Content.Shared.Verbs;
using Content.Shared.IdentityManagement;
using Content.Shared.Tools.EntitySystems;
using Content.Shared.Whitelist;
using Content.Shared.Materials;
using Robust.Shared.Map;
namespace Content.Shared.Storage.EntitySystems;
@@ -35,6 +37,7 @@ public sealed class SecretStashSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SecretStashComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
SubscribeLocalEvent<SecretStashComponent, GotReclaimedEvent>(OnReclaimed);
SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem) });
SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
@@ -47,12 +50,12 @@ public sealed class SecretStashSystem : EntitySystem
private void OnDestroyed(Entity<SecretStashComponent> entity, ref DestructionEventArgs args)
{
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer);
if (storedInside != null && storedInside.Count >= 1)
{
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
_popupSystem.PopupEntity(popup, storedInside[0], PopupType.MediumCaution);
}
DropContentsAndAlert(entity);
}
private void OnReclaimed(Entity<SecretStashComponent> entity, ref GotReclaimedEvent args)
{
DropContentsAndAlert(entity, args.ReclaimerCoordinates);
}
private void OnInteractUsing(Entity<SecretStashComponent> entity, ref InteractUsingEvent args)
@@ -211,5 +214,18 @@ public sealed class SecretStashSystem : EntitySystem
return entity.Comp.ItemContainer.ContainedEntity != null;
}
/// <summary>
/// Drop the item stored in the stash and alert all nearby players with a popup.
/// </summary>
private void DropContentsAndAlert(Entity<SecretStashComponent> entity, EntityCoordinates? cords = null)
{
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer, true, cords);
if (storedInside != null && storedInside.Count >= 1)
{
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
_popupSystem.PopupPredicted(popup, storedInside[0], null, PopupType.MediumCaution);
}
}
#endregion
}

View File

@@ -673,7 +673,7 @@ public abstract class SharedStorageSystem : EntitySystem
private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
{
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item, held: true))
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item))
return;
SaveItemLocation(storage!, item.Owner);

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