Merge remote-tracking branch 'refs/remotes/wizards/master'

# Conflicts:
#	Content.Client/Info/LinkBanner.cs
#	Resources/Locale/en-US/_strings/atmos/gas-pipe-sensor.ftl
#	Resources/Locale/en-US/_strings/station-events/events/greytide-virus.ftl
#	Resources/Prototypes/Datasets/Names/diona.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
This commit is contained in:
Vigers Ray 2024-11-26 17:50:59 +03:00
commit 0c1e15e516
165 changed files with 15734 additions and 1263 deletions

View file

@ -22,11 +22,11 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow
{
public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string>? PlayerChanged;
private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; }
private byte[]? Hwid { get; set; }
private ImmutableTypedHwid? Hwid { get; set; }
private double TimeEntered { get; set; }
private uint Multiplier { get; set; }
private bool HasBanFlag { get; set; }
@ -371,9 +371,8 @@ public sealed partial class BanPanel : DefaultWindow
private void OnHwidChanged()
{
var hwidString = HwidLine.Text;
var length = 3 * (hwidString.Length / 4) - hwidString.TakeLast(2).Count(c => c == '=');
Hwid = new byte[length];
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !Convert.TryFromBase64String(hwidString, Hwid, out _))
ImmutableTypedHwid? hwid = null;
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !ImmutableTypedHwid.TryParse(hwidString, out hwid))
{
ErrorLevel |= ErrorLevelEnum.Hwid;
HwidLine.ModulateSelfOverride = Color.Red;
@ -390,7 +389,7 @@ public sealed partial class BanPanel : DefaultWindow
Hwid = null;
return;
}
Hwid = Convert.FromHexString(hwidString);
Hwid = hwid;
}
private void OnTypeChanged()

View file

@ -159,6 +159,7 @@ public sealed partial class NoteEdit : FancyWindow
SecretCheckBox.Pressed = false;
SeverityOption.Disabled = false;
PermanentCheckBox.Pressed = true;
SubmitButton.Disabled = true;
UpdatePermanentCheckboxFields();
break;
case (int) NoteType.Message: // Message: these are shown to the player when they log on

View file

@ -0,0 +1,95 @@
using Content.Shared.Electrocution;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.Electrocution;
/// <summary>
/// Shows the Electrocution HUD to entities with the ShowElectrocutionHUDComponent.
/// </summary>
public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<ElectrocutionHUDVisualsComponent>
{
[Dependency] private readonly IPlayerManager _playerMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ShowElectrocutionHUDComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
}
private void OnPlayerAttached(Entity<ShowElectrocutionHUDComponent> ent, ref LocalPlayerAttachedEvent args)
{
ShowHUD();
}
private void OnPlayerDetached(Entity<ShowElectrocutionHUDComponent> ent, ref LocalPlayerDetachedEvent args)
{
RemoveHUD();
}
private void OnInit(Entity<ShowElectrocutionHUDComponent> ent, ref ComponentInit args)
{
if (_playerMan.LocalEntity == ent)
{
ShowHUD();
}
}
private void OnShutdown(Entity<ShowElectrocutionHUDComponent> ent, ref ComponentShutdown args)
{
if (_playerMan.LocalEntity == ent)
{
RemoveHUD();
}
}
// Show the HUD to the client.
// We have to look for all current entities that can be electrified and toggle the HUD layer on if they are.
private void ShowHUD()
{
var electrifiedQuery = AllEntityQuery<ElectrocutionHUDVisualsComponent, AppearanceComponent, SpriteComponent>();
while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
{
if (!AppearanceSystem.TryGetData<bool>(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp))
continue;
if (electrified)
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, true);
else
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}
// Remove the HUD from the client.
// Find all current entities that can be electrified and hide the HUD layer.
private void RemoveHUD()
{
var electrifiedQuery = AllEntityQuery<ElectrocutionHUDVisualsComponent, AppearanceComponent, SpriteComponent>();
while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
{
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}
// Toggle the HUD layer if an entity becomes (de-)electrified
protected override void OnAppearanceChange(EntityUid uid, ElectrocutionHUDVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!AppearanceSystem.TryGetData<bool>(uid, ElectrifiedVisuals.IsElectrified, out var electrified, args.Component))
return;
var player = _playerMan.LocalEntity;
if (electrified && HasComp<ShowElectrocutionHUDComponent>(player))
args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, true);
else
args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}

View file

@ -55,7 +55,7 @@ namespace Content.Client.Eui
/// </summary>
protected void SendMessage(EuiMessageBase msg)
{
var netMsg = _netManager.CreateNetMessage<MsgEuiMessage>();
var netMsg = new MsgEuiMessage();
netMsg.Id = Id;
netMsg.Message = msg;

View file

@ -37,9 +37,7 @@ namespace Content.Client.Info
AddInfoButton("server-info-website-button", CCVars.InfoLinksWebsite);
AddInfoButton("server-info-wiki-button", CCVars.InfoLinksWiki);
AddInfoButton("server-info-forum-button", CCVars.InfoLinksForum);
// Sunrise-Start
AddInfoButton("server-info-telegram-button", SunriseCCVars.InfoLinksTelegram);
// Sunrise-End
AddInfoButton("server-info-telegram-button", CCVars.InfoLinksTelegram);
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var guidebookButton = new Button() { Text = Loc.GetString("server-info-guidebook-button") };

View file

@ -116,7 +116,7 @@ public partial class BaseShuttleControl : MapGridControl
}
}
protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 matrix, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
{
var rator = Maps.GetAllTilesEnumerator(grid.Owner, grid.Comp);
var minimapScale = MinimapScale;
@ -264,7 +264,7 @@ public partial class BaseShuttleControl : MapGridControl
Extensions.EnsureLength(ref _allVertices, totalData);
_drawJob.MidPoint = midpoint;
_drawJob.Matrix = matrix;
_drawJob.Matrix = gridToView;
_drawJob.MinimapScale = minimapScale;
_drawJob.Vertices = gridData.Vertices;
_drawJob.ScaledVertices = _allVertices;
@ -286,7 +286,7 @@ public partial class BaseShuttleControl : MapGridControl
private record struct GridDrawJob : IParallelRobustJob
{
public int BatchSize => 16;
public int BatchSize => 64;
public float MinimapScale;
public Vector2 MidPoint;
@ -297,12 +297,7 @@ public partial class BaseShuttleControl : MapGridControl
public void Execute(int index)
{
var vert = Vertices[index];
var adjustedVert = Vector2.Transform(vert, Matrix);
adjustedVert = adjustedVert with { Y = -adjustedVert.Y };
var scaledVert = ScalePosition(adjustedVert, MinimapScale, MidPoint);
ScaledVertices[index] = scaledVert;
ScaledVertices[index] = Vector2.Transform(Vertices[index], Matrix);
}
}
}

View file

@ -15,6 +15,7 @@ public sealed partial class NavScreen : BoxContainer
[Dependency] private readonly IEntityManager _entManager = default!;
private SharedTransformSystem _xformSystem;
private EntityUid? _consoleEntity; // Entity of controlling console
private EntityUid? _shuttleEntity;
public NavScreen()
@ -35,6 +36,12 @@ public sealed partial class NavScreen : BoxContainer
_shuttleEntity = shuttle;
}
public void SetConsole(EntityUid? console)
{
_consoleEntity = console;
NavRadar.SetConsole(console);
}
private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
{
NavRadar.ShowIFF ^= true;

View file

@ -138,6 +138,7 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
{
var coordinates = _entManager.GetCoordinates(cState.NavState.Coordinates);
NavContainer.SetShuttle(coordinates?.EntityId);
NavContainer.SetConsole(owner);
MapContainer.SetShuttle(coordinates?.EntityId);
MapContainer.SetConsole(owner);

View file

@ -107,16 +107,19 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
DrawCircles(handle);
var gridNent = EntManager.GetNetEntity(GridEntity);
var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
var ourGridMatrix = _xformSystem.GetWorldMatrix(GridEntity.Value);
var dockMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero);
var worldFromDock = Matrix3x2.Multiply(dockMatrix, ourGridMatrix);
var ourGridToWorld = _xformSystem.GetWorldMatrix(GridEntity.Value);
var selectedDockToOurGrid = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero);
var selectedDockToWorld = Matrix3x2.Multiply(selectedDockToOurGrid, ourGridToWorld);
Matrix3x2.Invert(worldFromDock, out var offsetMatrix);
Box2 viewBoundsWorld = Matrix3Helpers.TransformBox(selectedDockToWorld, new Box2(-WorldRangeVector, WorldRangeVector));
Matrix3x2.Invert(selectedDockToWorld, out var worldToSelectedDock);
var selectedDockToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
// Draw nearby grids
var controlBounds = PixelSizeBox;
_grids.Clear();
_mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(mapPos.Position - WorldRangeVector, mapPos.Position + WorldRangeVector), ref _grids);
_mapManager.FindGridsIntersecting(gridXform.MapID, viewBoundsWorld, ref _grids);
// offset the dotted-line position to the bounds.
Vector2? viewedDockPos = _viewedState != null ? MidPointVector : null;
@ -136,11 +139,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (grid.Owner != GridEntity && !_shuttles.CanDraw(grid.Owner, iffComp: iffComp))
continue;
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, offsetMatrix);
var curGridToWorld = _xformSystem.GetWorldMatrix(grid.Owner);
var curGridToView = curGridToWorld * worldToSelectedDock * selectedDockToView;
var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp);
DrawGrid(handle, matty, grid, color);
DrawGrid(handle, curGridToView, grid, color);
// Draw any docks on that grid
if (!DockState.Docks.TryGetValue(EntManager.GetNetEntity(grid), out var gridDocks))
@ -151,23 +154,24 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (ViewedDock == dock.Entity)
continue;
var position = Vector2.Transform(dock.Coordinates.Position, matty);
var otherDockRotation = Matrix3Helpers.CreateRotation(dock.Angle);
var scaledPos = ScalePosition(position with {Y = -position.Y});
if (!controlBounds.Contains(scaledPos.Floored()))
// This box is the AABB of all the vertices we draw below.
var dockRenderBoundsLocal = new Box2(-0.5f, -0.7f, 0.5f, 0.5f);
var currentDockToCurGrid = Matrix3Helpers.CreateTransform(dock.Coordinates.Position, dock.Angle);
var currentDockToWorld = Matrix3x2.Multiply(currentDockToCurGrid, curGridToWorld);
var dockRenderBoundsWorld = Matrix3Helpers.TransformBox(currentDockToWorld, dockRenderBoundsLocal);
if (!viewBoundsWorld.Intersects(dockRenderBoundsWorld))
continue;
// Draw the dock's collision
var collisionBL = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), matty);
Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), curGridToView);
var collisionBR = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), matty);
Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), curGridToView);
var collisionTR = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), matty);
Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), curGridToView);
var collisionTL = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), matty);
Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), curGridToView);
var verts = new[]
{
@ -181,13 +185,6 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
collisionBL,
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
var collisionCenter = verts[0] + verts[1] + verts[3] + verts[5];
var otherDockConnection = Color.ToSrgb(Color.Pink);
@ -195,10 +192,10 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockConnection);
// Draw the dock itself
var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), matty);
var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), matty);
var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), matty);
var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), matty);
var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), curGridToView);
var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), curGridToView);
var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), curGridToView);
var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), curGridToView);
verts = new[]
{
@ -212,13 +209,6 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
dockBL
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
Color otherDockColor;
if (HighlightedDock == dock.Entity)
@ -253,9 +243,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
collisionCenter /= 4;
var range = viewedDockPos.Value - collisionCenter;
if (range.Length() < SharedDockingSystem.DockingHiglightRange * MinimapScale)
var maxRange = SharedDockingSystem.DockingHiglightRange * MinimapScale;
var maxRangeSq = maxRange * maxRange;
if (range.LengthSquared() < maxRangeSq)
{
if (_viewedState?.GridDockedWith == null)
if (dock.GridDockedWith == null)
{
var coordsOne = EntManager.GetCoordinates(_viewedState!.Coordinates);
var coordsTwo = EntManager.GetCoordinates(dock.Coordinates);
@ -265,10 +257,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
var rotA = _xformSystem.GetWorldRotation(coordsOne.EntityId) + _viewedState!.Angle;
var rotB = _xformSystem.GetWorldRotation(coordsTwo.EntityId) + dock.Angle;
var distance = (mapOne.Position - mapTwo.Position).Length();
var distanceSq = (mapOne.Position - mapTwo.Position).LengthSquared();
var inAlignment = _dockSystem.InAlignment(mapOne, rotA, mapTwo, rotB);
var canDock = distance < SharedDockingSystem.DockRange && inAlignment;
var maxDockDistSq = SharedDockingSystem.DockRange * SharedDockingSystem.DockRange;
var canDock = distanceSq < maxDockDistSq && inAlignment;
if (dockButton != null)
dockButton.Disabled = !canDock || !canDockChange;
@ -297,7 +290,8 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
{
// Because it's being layed out top-down we have to arrange for first frame.
container.Arrange(PixelRect);
var containerPos = scaledPos / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
var dockPositionInView = Vector2.Transform(dock.Coordinates.Position, curGridToView);
var containerPos = dockPositionInView / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
SetPosition(container, containerPos);
}

View file

@ -29,6 +29,11 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
/// </summary>
private EntityCoordinates? _coordinates;
/// <summary>
/// Entity of controlling console
/// </summary>
private EntityUid? _consoleEntity;
private Angle? _rotation;
private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
@ -57,6 +62,11 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
_rotation = angle;
}
public void SetConsole(EntityUid? consoleEntity)
{
_consoleEntity = consoleEntity;
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
@ -139,40 +149,35 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
}
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
var offset = _coordinates.Value.Position;
var posMatrix = Matrix3Helpers.CreateTransform(offset, _rotation.Value);
var posMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, _rotation.Value);
var ourEntRot = RotateWithEntity ? _transform.GetWorldRotation(xform) : _rotation.Value;
var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
var ourWorldMatrix = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(ourWorldMatrix, out var ourWorldMatrixInvert);
var shuttleToWorld = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(shuttleToWorld, out var worldToShuttle);
var shuttleToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
// Draw our grid in detail
var ourGridId = xform.GridUid;
if (EntManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
fixturesQuery.HasComponent(ourGridId.Value))
{
var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
var matrix = Matrix3x2.Multiply(ourGridMatrix, ourWorldMatrixInvert);
var ourGridToWorld = _transform.GetWorldMatrix(ourGridId.Value);
var ourGridToShuttle = Matrix3x2.Multiply(ourGridToWorld, worldToShuttle);
var ourGridToView = ourGridToShuttle * shuttleToView;
var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
DrawGrid(handle, matrix, (ourGridId.Value, ourGrid), color);
DrawDocks(handle, ourGridId.Value, matrix);
DrawGrid(handle, ourGridToView, (ourGridId.Value, ourGrid), color);
DrawDocks(handle, ourGridId.Value, ourGridToView);
}
var invertedPosition = _coordinates.Value.Position - offset;
invertedPosition.Y = -invertedPosition.Y;
// Don't need to transform the InvWorldMatrix again as it's already offset to its position.
// Draw radar position on the station
var radarPos = invertedPosition;
const float radarVertRadius = 2f;
var radarPosVerts = new Vector2[]
{
ScalePosition(radarPos + new Vector2(0f, -radarVertRadius)),
ScalePosition(radarPos + new Vector2(radarVertRadius / 2f, 0f)),
ScalePosition(radarPos + new Vector2(0f, radarVertRadius)),
ScalePosition(radarPos + new Vector2(radarVertRadius / -2f, 0f)),
ScalePosition(new Vector2(0f, -radarVertRadius)),
ScalePosition(new Vector2(radarVertRadius / 2f, 0f)),
ScalePosition(new Vector2(0f, radarVertRadius)),
ScalePosition(new Vector2(radarVertRadius / -2f, 0f)),
};
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
@ -197,8 +202,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
if (!_shuttles.CanDraw(gUid, gridBody, iff))
continue;
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
var curGridToWorld = _transform.GetWorldMatrix(gUid);
var curGridToView = curGridToWorld * worldToShuttle * shuttleToView;
var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
@ -213,8 +218,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
{
var gridBounds = grid.Comp.LocalAABB;
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
@ -230,9 +234,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
// The actual position in the UI. We centre the label by offsetting the matrix position
// by half the label's width, plus the y-offset
var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
// The actual position in the UI.
var gridScaledPosition = gridCentre - new Vector2(0, -yOffset);
// Normalize the grid position if it exceeds the viewport bounds
// normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
@ -264,18 +267,32 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
}
// Detailed view
var gridAABB = gridMatrix.TransformBox(grid.Comp.LocalAABB);
var gridAABB = curGridToWorld.TransformBox(grid.Comp.LocalAABB);
// Skip drawing if it's out of range.
if (!gridAABB.Intersects(viewAABB))
continue;
DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
DrawGrid(handle, curGridToView, grid, labelColor);
DrawDocks(handle, gUid, curGridToView);
}
// If we've set the controlling console, and it's on a different grid
// to the shuttle itself, then draw an additional marker to help the
// player determine where they are relative to the shuttle.
if (_consoleEntity != null && xformQuery.TryGetComponent(_consoleEntity, out var consoleXform))
{
if (consoleXform.ParentUid != _coordinates.Value.EntityId)
{
var consolePositionWorld = _transform.GetWorldPosition((EntityUid)_consoleEntity);
var p = Vector2.Transform(consolePositionWorld, worldToShuttle * shuttleToView);
handle.DrawCircle(p, 5, Color.ToSrgb(Color.Cyan), true);
}
}
}
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 matrix)
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 gridToView)
{
if (!ShowDocks)
return;
@ -283,33 +300,32 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
const float DockScale = 0.6f;
var nent = EntManager.GetNetEntity(uid);
const float sqrt2 = 1.41421356f;
const float dockRadius = DockScale * sqrt2;
// Worst-case bounds used to cull a dock:
Box2 viewBounds = new Box2(-dockRadius, -dockRadius, Size.X + dockRadius, Size.Y + dockRadius);
if (_docks.TryGetValue(nent, out var docks))
{
foreach (var state in docks)
{
var position = state.Coordinates.Position;
var uiPosition = Vector2.Transform(position, matrix);
if (uiPosition.Length() > (WorldRange * 2f) - DockScale)
var positionInView = Vector2.Transform(position, gridToView);
if (!viewBounds.Contains(positionInView))
{
continue;
}
var color = Color.ToSrgb(Color.Magenta);
var verts = new[]
{
Vector2.Transform(position + new Vector2(-DockScale, -DockScale), matrix),
Vector2.Transform(position + new Vector2(DockScale, -DockScale), matrix),
Vector2.Transform(position + new Vector2(DockScale, DockScale), matrix),
Vector2.Transform(position + new Vector2(-DockScale, DockScale), matrix),
Vector2.Transform(position + new Vector2(-DockScale, -DockScale), gridToView),
Vector2.Transform(position + new Vector2(DockScale, -DockScale), gridToView),
Vector2.Transform(position + new Vector2(DockScale, DockScale), gridToView),
Vector2.Transform(position + new Vector2(-DockScale, DockScale), gridToView),
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
}

View file

@ -18,9 +18,8 @@ using static Robust.Client.UserInterface.Controls.LineEdit;
namespace Content.Client.UserInterface.Systems.Chat.Widgets;
[GenerateTypedNameReferences]
#pragma warning disable RA0003
[Virtual]
public partial class ChatBox : UIWidget
#pragma warning restore RA0003
{
private readonly ChatUIController _controller;
private readonly IEntityManager _entManager;

View file

@ -71,6 +71,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
buttonHeading.AddStyleClass(ContainerButton.StyleClassButton);
buttonHeading.Label.HorizontalAlignment = HAlignment.Center;
buttonHeading.Label.HorizontalExpand = true;
buttonHeading.Margin = new Thickness(8, 0, 8, 2);
var body = new CollapsibleBody
{

View file

@ -11,17 +11,21 @@
Orientation="Horizontal"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SeparationOverride="5"
>
<GridContainer
Rows="2"
HSeparationOverride="5"
VSeparationOverride="5"
HorizontalExpand="True">
<ui:MenuButton
Name="EscapeButton"
Access="Internal"
Icon="{xe:Tex '/Textures/Interface/hamburger.svg.192dpi.png'}"
BoundKey = "{x:Static ic:EngineKeyFunctions.EscapeMenu}"
ToolTip="{Loc 'game-hud-open-escape-menu-button-tooltip'}"
MinSize="70 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
<ui:MenuButton
Name="GuidebookButton"
@ -29,7 +33,7 @@
Icon="{xe:Tex '/Textures/Interface/VerbIcons/information.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -39,7 +43,7 @@
Icon="{xe:Tex '/Textures/Interface/character.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -49,7 +53,7 @@
Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -59,7 +63,7 @@
Icon="{xe:Tex '/Textures/Interface/hammer.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -69,7 +73,7 @@
Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -79,7 +83,7 @@
Icon="{xe:Tex '/Textures/Interface/gavel.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -89,7 +93,7 @@
Icon="{xe:Tex '/Textures/Interface/sandbox.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@ -99,8 +103,9 @@
Icon="{xe:Tex '/Textures/Interface/info.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
</GridContainer>
</widgets:GameTopMenuBar>

View file

@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands
// No bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
// Try to pardon a ban that does not exist
@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Still no bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
var banReason = "test";
@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Should have one ban on record now
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
await pair.RunTicksSync(5);
@ -70,13 +70,13 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
// The existing ban is unaffected
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
var ban = await sDatabase.GetServerBanAsync(1);
Assert.Multiple(async () =>
{
Assert.That(ban, Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
// Check that it matches
Assert.That(ban.Id, Is.EqualTo(1));
@ -95,7 +95,7 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
var pardonedBan = await sDatabase.GetServerBanAsync(1);
@ -105,7 +105,7 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.That(pardonedBan, Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
Assert.That(pardonedBan.Id, Is.EqualTo(1));
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.Multiple(async () =>
{
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
// Reconnect client. Slightly faster than dirtying the pair.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "integer",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "real",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View file

@ -512,20 +512,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@ -571,6 +557,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@ -589,10 +588,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("smallint")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
@ -603,6 +598,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("real")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
@ -718,10 +717,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("inet")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_time");
@ -1065,10 +1060,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@ -1199,10 +1190,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@ -1644,6 +1631,34 @@ namespace Content.Server.Database.Migrations.Postgres
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("integer")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@ -1659,6 +1674,37 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("integer")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@ -1753,8 +1799,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("integer")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@ -1802,8 +1876,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("integer")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "INTEGER",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View file

@ -483,19 +483,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@ -539,6 +526,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@ -555,10 +555,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@ -569,6 +565,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("REAL")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
@ -675,10 +675,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("TEXT")
.HasColumnName("last_seen_time");
@ -1003,10 +999,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@ -1131,10 +1123,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@ -1566,6 +1554,34 @@ namespace Content.Server.Database.Migrations.Sqlite
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("INTEGER")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@ -1581,6 +1597,37 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("INTEGER")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@ -1675,8 +1722,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@ -1724,8 +1799,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

View file

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Text.Json;
@ -327,6 +329,47 @@ namespace Content.Server.Database
.HasForeignKey(w => w.PlayerUserId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Changes for modern HWID integration
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Hwid)
.HasColumnName("last_seen_hwid");
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@ -520,7 +563,7 @@ namespace Content.Server.Database
public string LastSeenUserName { get; set; } = null!;
public DateTime LastSeenTime { get; set; }
public IPAddress LastSeenAddress { get; set; } = null!;
public byte[]? LastSeenHWId { get; set; }
public TypedHwid? LastSeenHWId { get; set; }
// Data that changes with each round
public List<Round> Rounds { get; set; } = null!;
@ -669,7 +712,7 @@ namespace Content.Server.Database
int Id { get; set; }
Guid? PlayerUserId { get; set; }
NpgsqlInet? Address { get; set; }
byte[]? HWId { get; set; }
TypedHwid? HWId { get; set; }
DateTime BanTime { get; set; }
DateTime? ExpirationTime { get; set; }
string Reason { get; set; }
@ -754,7 +797,7 @@ namespace Content.Server.Database
/// <summary>
/// Hardware ID of the banned player.
/// </summary>
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
@ -892,7 +935,7 @@ namespace Content.Server.Database
public DateTime Time { get; set; }
public IPAddress Address { get; set; } = null!;
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public ConnectionDenyReason? Denied { get; set; }
@ -909,6 +952,8 @@ namespace Content.Server.Database
public List<ServerBanHit> BanHits { get; set; } = null!;
public Server Server { get; set; } = null!;
public float Trust { get; set; }
}
public enum ConnectionDenyReason : byte
@ -946,7 +991,7 @@ namespace Content.Server.Database
public Guid? PlayerUserId { get; set; }
[Required] public TimeSpan PlaytimeAtNote { get; set; }
public NpgsqlInet? Address { get; set; }
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public DateTime BanTime { get; set; }
@ -1207,4 +1252,37 @@ namespace Content.Server.Database
/// <seealso cref="ServerBan.Hidden"/>
public bool Hidden { get; set; }
}
/// <summary>
/// A hardware ID value together with its <see cref="HwidType"/>.
/// </summary>
/// <seealso cref="ImmutableTypedHwid"/>
[Owned]
public sealed class TypedHwid
{
public byte[] Hwid { get; set; } = default!;
public HwidType Type { get; set; }
[return: NotNullIfNotNull(nameof(immutable))]
public static implicit operator TypedHwid?(ImmutableTypedHwid? immutable)
{
if (immutable == null)
return null;
return new TypedHwid
{
Hwid = immutable.Hwid.ToArray(),
Type = immutable.Type,
};
}
[return: NotNullIfNotNull(nameof(hwid))]
public static implicit operator ImmutableTypedHwid?(TypedHwid? hwid)
{
if (hwid == null)
return null;
return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
}
}
}

View file

@ -82,7 +82,7 @@ namespace Content.Server.Database
}
}
public class SnakeCaseConvention :
public partial class SnakeCaseConvention :
IEntityTypeAddedConvention,
IEntityTypeAnnotationChangedConvention,
IPropertyAddedConvention,
@ -99,22 +99,27 @@ namespace Content.Server.Database
public static string RewriteName(string name)
{
var regex = new Regex("[A-Z]+", RegexOptions.Compiled);
return regex.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
return UpperCaseLocator()
.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
// Do not add a _ if there is already one before this. This happens with owned entities.
if (name[match.Index - 1] == '_')
return match.Value.ToLower();
return "_" + match.Value.ToLower();
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
return "_" + match.Value.ToLower();
}
);
);
}
public virtual void ProcessEntityTypeAdded(
@ -332,5 +337,8 @@ namespace Content.Server.Database
}
}
}
[GeneratedRegex("[A-Z]+", RegexOptions.Compiled)]
private static partial Regex UpperCaseLocator();
}
}

View file

@ -54,7 +54,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@ -74,7 +74,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
Bans.Add(new SharedServerBan(
@ -95,7 +95,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadRoleBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@ -115,7 +115,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
RoleBans.Add(new SharedServerRoleBan(
ban.Id,

View file

@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.Sockets;
using Content.Server.Administration.Managers;
@ -8,7 +7,6 @@ using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Administration;
@ -27,7 +25,7 @@ public sealed class BanPanelEui : BaseEui
private NetUserId? PlayerId { get; set; }
private string PlayerName { get; set; } = string.Empty;
private IPAddress? LastAddress { get; set; }
private ImmutableArray<byte>? LastHwid { get; set; }
private ImmutableTypedHwid? LastHwid { get; set; }
private const int Ipv4_CIDR = 32;
private const int Ipv6_CIDR = 64;
@ -51,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg)
{
case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername);
@ -59,7 +57,7 @@ public sealed class BanPanelEui : BaseEui
}
}
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
{
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
@ -156,7 +154,7 @@ public sealed class BanPanelEui : BaseEui
ChangePlayer(located?.UserId, located?.Username ?? string.Empty, located?.LastAddress, located?.LastHWId);
}
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableArray<byte>? lastHwid)
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableTypedHwid? lastHwid)
{
PlayerId = playerId;
PlayerName = playerName;

View file

@ -38,7 +38,7 @@ public sealed class BanListCommand : LocalizedCommands
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastHWId, false);
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
if (bans.Count == 0)
{

View file

@ -48,7 +48,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastHWId, includeUnbanned);
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned);
if (bans.Count == 0)
{

View file

@ -100,7 +100,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
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 modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
@ -167,7 +168,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#region Server Bans
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason)
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
{
DateTimeOffset? expires = null;
if (minutes > 0)
@ -207,9 +208,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var addressRangeString = addressRange != null
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
: "null";
var hwidString = hwid != null
? string.Concat(hwid.Value.Select(x => x.ToString("x2")))
: "null";
var hwidString = hwid?.ToString() ?? "null";
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
@ -256,6 +255,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
ModernHWIds = player.Channel.UserData.ModernHWIds,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
@ -276,7 +276,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#region Job Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
{
if (!_prototypeManager.TryIndex(role, out JobPrototype? _))
{

View file

@ -24,7 +24,7 @@ public interface IBanManager
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
@ -37,7 +37,7 @@ public interface IBanManager
/// <param name="reason">Reason for the ban</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
public void WebhookUpdateRoleBans(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, IReadOnlyCollection<string> roles, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);

View file

@ -5,16 +5,42 @@ using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration
{
public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray<byte>? LastHWId, string Username);
/// <summary>
/// Contains data resolved via <see cref="IPlayerLocator"/>.
/// </summary>
/// <param name="UserId">The ID of the located user.</param>
/// <param name="LastAddress">The last known IP address that the user connected with.</param>
/// <param name="LastHWId">
/// The last known HWID that the user connected with.
/// This should be used for placing new records involving HWIDs, such as bans.
/// For looking up data based on HWID, use combined <see cref="LastLegacyHWId"/> and <see cref="LastModernHWIds"/>.
/// </param>
/// <param name="Username">The last known username for the user connected with.</param>
/// <param name="LastLegacyHWId">
/// The last known legacy HWID value this user connected with. Only use for old lookups!
/// </param>
/// <param name="LastModernHWIds">
/// The set of last known modern HWIDs the user connected with.
/// </param>
public sealed record LocatedPlayerData(
NetUserId UserId,
IPAddress? LastAddress,
ImmutableTypedHwid? LastHWId,
string Username,
ImmutableArray<byte>? LastLegacyHWId,
ImmutableArray<ImmutableArray<byte>> LastModernHWIds);
/// <summary>
/// Utilities for finding user IDs that extend to more than the server database.
@ -67,63 +93,42 @@ namespace Content.Server.Administration
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionByUsername(playerName, out var session))
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserName(playerName, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
{
_sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode);
return null;
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return await HandleAuthServerResponse(resp, cancel);
}
public async Task<LocatedPlayerData?> LookupIdAsync(NetUserId userId, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionById(userId, out var session))
{
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserId(userId, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
private async Task<LocatedPlayerData?> HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel)
{
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
@ -134,14 +139,40 @@ namespace Content.Server.Administration
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []);
}
private static LocatedPlayerData ReturnForSession(ICommonSession session)
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.GetModernHwid();
return new LocatedPlayerData(
userId,
address,
hwId,
session.Name,
session.Channel.UserData.HWId,
session.Channel.UserData.ModernHWIds);
}
private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record)
{
var hwid = record.HWId;
return new LocatedPlayerData(
record.UserId,
record.LastSeenAddress,
hwid,
record.LastSeenUserName,
hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null,
hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []);
}
public async Task<LocatedPlayerData?> LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default)

View file

@ -173,11 +173,11 @@ public sealed class PlayerPanelEui : BaseEui
{
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count;
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
// The only way to distinguish whether a role ban is the same is to compare the ban time.
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count();
}
else
{

View file

@ -176,7 +176,7 @@ namespace Content.Server.Administration.Systems
}
// Check if the user has been banned
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null);
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
if (ban != null)
{
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));

View file

@ -48,7 +48,9 @@ public sealed partial class AtmosMonitorComponent : Component
[DataField("gasThresholds")]
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
// Stores a reference to the gas on the tile this is on.
/// <summary>
/// Stores a reference to the gas on the tile this entity is on (or the pipe network it monitors; see <see cref="MonitorsPipeNet"/>).
/// </summary>
[ViewVariables]
public GasMixture? TileGas;
@ -65,4 +67,19 @@ public sealed partial class AtmosMonitorComponent : Component
/// </summary>
[DataField("registeredDevices")]
public HashSet<string> RegisteredDevices = new();
/// <summary>
/// Specifies whether this device monitors its own internal pipe network rather than the surrounding atmosphere.
/// </summary>
/// <remarks>
/// If 'true', the entity will require a NodeContainerComponent with one or more PipeNodes to function.
/// </remarks>
[DataField]
public bool MonitorsPipeNet = false;
/// <summary>
/// Specifies the name of the pipe node that this device is monitoring.
/// </summary>
[DataField]
public string NodeNameMonitoredPipe = "monitored";
}

View file

@ -4,6 +4,9 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
@ -25,6 +28,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainerSystem = default!;
// Commands
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
@ -56,8 +60,15 @@ public sealed class AtmosMonitorSystem : EntitySystem
private void OnAtmosDeviceEnterAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceEnabledEvent args)
{
if (atmosMonitor.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, atmosMonitor.NodeNameMonitoredPipe, out var pipeNode))
{
atmosMonitor.TileGas = pipeNode.Air;
return;
}
atmosMonitor.TileGas = _atmosphereSystem.GetContainingMixture(uid, true);
}
private void OnMapInit(EntityUid uid, AtmosMonitorComponent component, MapInitEvent args)
{
if (component.TemperatureThresholdId != null)
@ -206,7 +217,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Grid == null)
if (args.Grid == null)
return;
// if we're not monitoring atmos, don't bother
@ -215,6 +226,10 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.GasThresholds == null)
return;
// If monitoring a pipe network, get its most recent gas mixture
if (component.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, component.NodeNameMonitoredPipe, out var pipeNode))
component.TileGas = pipeNode.Air;
UpdateState(uid, component.TileGas, component);
}

View file

@ -116,11 +116,14 @@ namespace Content.Server.Connection
var serverId = (await _serverDbEntry.ServerEntity).Id;
var hwid = e.UserData.GetModernHwid();
var trust = e.UserData.Trust;
if (deny != null)
{
var (reason, msg, banHits) = deny.Value;
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId);
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId);
if (banHits is { Count: > 0 })
await _db.AddServerBanHitsAsync(id, banHits);
@ -132,12 +135,12 @@ namespace Content.Server.Connection
}
else
{
await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId);
await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId);
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
return;
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId);
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid);
}
}
@ -195,7 +198,9 @@ namespace Content.Server.Connection
hwId = null;
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
var modernHwid = e.UserData.ModernHWIds;
var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
if (bans.Count > 0)
{
var firstBan = bans[0];

View file

@ -0,0 +1,24 @@
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection;
/// <summary>
/// Helper functions for working with <see cref="NetUserData"/>.
/// </summary>
public static class UserDataExt
{
/// <summary>
/// Get the preferred HWID that should be used for new records related to a player.
/// </summary>
/// <remarks>
/// Players can have zero or more HWIDs, but for logging things like connection logs we generally
/// only want a single one. This method returns a nullable method.
/// </remarks>
public static ImmutableTypedHwid? GetModernHwid(this NetUserData userData)
{
return userData.ModernHWIds.Length == 0
? null
: new ImmutableTypedHwid(userData.ModernHWIds[0], HwidType.Modern);
}
}

View file

@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Net;
using Content.Server.IP;
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Database;
@ -52,9 +53,28 @@ public static class BanMatcher
return true;
}
return player.HWId is { Length: > 0 } hwIdVar
&& ban.HWId != null
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (player.HWId is { Length: > 0 } hwIdVar
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
{
return true;
}
break;
case HwidType.Modern:
if (player.ModernHWIds is { Length: > 0 } modernHwIdVar)
{
foreach (var hwid in modernHwIdVar)
{
if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
return true;
}
}
break;
}
return false;
}
/// <summary>
@ -73,10 +93,15 @@ public static class BanMatcher
public IPAddress? Address;
/// <summary>
/// The hardware ID of the player.
/// The LEGACY hardware ID of the player. Corresponds with <see cref="NetUserData.HWId"/>.
/// </summary>
public ImmutableArray<byte>? HWId;
/// <summary>
/// The modern hardware IDs of the player. Corresponds with <see cref="NetUserData.ModernHWIds"/>.
/// </summary>
public ImmutableArray<ImmutableArray<byte>>? ModernHWIds;
/// <summary>
/// Exemption flags the player has been granted.
/// </summary>

View file

@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@ -121,7 +120,7 @@ public sealed record PlayerRecord(
string LastSeenUserName,
DateTimeOffset LastSeenTime,
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
ImmutableTypedHwid? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);

View file

@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
@ -13,7 +12,7 @@ namespace Content.Server.Database
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@ -28,7 +27,7 @@ namespace Content.Server.Database
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
TypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View file

@ -396,12 +396,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@ -410,13 +412,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns>
public abstract Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
@ -507,11 +511,13 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
@ -601,7 +607,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
await using var db = await GetDb();
@ -618,7 +624,7 @@ namespace Content.Server.Database
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
record.LastSeenHWId = hwId.ToArray();
record.LastSeenHWId = hwId;
await db.DbContext.SaveChangesAsync();
}
@ -664,7 +670,7 @@ namespace Content.Server.Database
player.LastSeenUserName,
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
player.LastSeenAddress,
player.LastSeenHWId?.ToImmutableArray());
player.LastSeenHWId);
}
#endregion
@ -673,11 +679,11 @@ namespace Content.Server.Database
/*
* CONNECTION LOG
*/
public abstract Task<int> AddConnectionLogAsync(
NetUserId userId,
public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);

View file

@ -69,12 +69,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The hardware ID of the user.</param>
/// <param name="hwId">The legacy HWID of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@ -82,13 +84,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
/// <returns>The user's ban history.</returns>
Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true);
Task AddServerBanAsync(ServerBanDef serverBan);
@ -137,12 +141,14 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
@ -180,7 +186,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId);
ImmutableTypedHwid? hwId);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
@ -191,7 +197,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);
@ -480,20 +487,22 @@ namespace Content.Server.Database
public Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId));
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task AddServerBanAsync(ServerBanDef serverBan)
@ -537,10 +546,11 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
@ -582,7 +592,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
@ -604,12 +614,13 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId));
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
}
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@ -73,7 +74,8 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address == null && userId == null && hwId == null)
{
@ -84,7 +86,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@ -94,7 +96,9 @@ namespace Content.Server.Database
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
@ -105,7 +109,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@ -127,6 +131,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags,
@ -134,16 +139,11 @@ namespace Content.Server.Database
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.Ban);
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP))
{
@ -156,15 +156,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
@ -186,6 +177,49 @@ namespace Content.Server.Database
return query.Distinct();
}
private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>(
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbSet<TBan> set)
where TBan : class, IBanCommon<TUnban>
where TUnban : class, IUnbanCommon
{
IQueryable<TBan>? query = null;
if (userId is { } uid)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (modernHWIds != null)
{
foreach (var modernHwid in modernHWIds)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
}
return query;
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)
@ -211,7 +245,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@ -249,7 +283,7 @@ namespace Content.Server.Database
db.PgDbContext.Ban.Add(new ServerBan
{
Address = serverBan.Address.ToNpgsqlInet(),
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
@ -297,6 +331,7 @@ namespace Content.Server.Database
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
@ -306,7 +341,7 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned)
var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
@ -334,19 +369,15 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.RoleBan);
if (address != null)
{
@ -357,15 +388,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
@ -402,7 +424,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@ -440,7 +462,7 @@ namespace Content.Server.Database
var ban = new ServerRoleBan
{
Address = serverRoleBan.Address.ToNpgsqlInet(),
HWId = serverRoleBan.HWId?.ToArray(),
HWId = serverRoleBan.HWId,
Reason = serverRoleBan.Reason,
Severity = serverRoleBan.Severity,
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
@ -476,7 +498,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@ -488,9 +511,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.PgDbContext.ConnectionLog.Add(connectionLog);

View file

@ -9,6 +9,7 @@ using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@ -80,22 +81,24 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned: false)).FirstOrDefault();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned)).ToList();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList();
}
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
@ -103,6 +106,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
var exempt = await GetBanExemptionCore(db, userId);
@ -119,6 +123,7 @@ namespace Content.Server.Database
UserId = userId,
ExemptFlags = exempt ?? default,
HWId = hwId,
ModernHWIds = modernHWIds,
IsNewPlayer = newPlayer,
};
@ -161,7 +166,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@ -205,6 +210,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
@ -214,7 +220,7 @@ namespace Content.Server.Database
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => RoleBanMatches(b, address, userId, hwId))
.Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
.Select(ConvertRoleBan)
.ToList()!;
}
@ -237,7 +243,8 @@ namespace Content.Server.Database
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
{
@ -249,7 +256,27 @@ namespace Content.Server.Database
return true;
}
return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId);
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
break;
case HwidType.Modern:
if (modernHWIds != null)
{
foreach (var modernHWId in modernHWIds)
{
if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
}
}
break;
}
return false;
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
@ -262,7 +289,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@ -316,7 +343,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@ -376,7 +403,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@ -412,7 +439,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@ -424,9 +452,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.SqliteDbContext.ConnectionLog.Add(connectionLog);

View file

@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@ -10,7 +9,7 @@ public sealed class ServerRoleBanDef
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@ -26,7 +25,7 @@ public sealed class ServerRoleBanDef
int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
ImmutableTypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View file

@ -121,7 +121,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
activated.TimeLeft -= frameTime;
if (activated.TimeLeft <= 0 || !IsPowered(uid, electrified, transform))
{
_appearance.SetData(uid, ElectrifiedVisuals.IsPowered, false);
_appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, false);
RemComp<ActivatedElectrifiedComponent>(uid);
}
}
@ -217,7 +217,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
return false;
EnsureComp<ActivatedElectrifiedComponent>(uid);
_appearance.SetData(uid, ElectrifiedVisuals.IsPowered, true);
_appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, true);
siemens *= electrified.SiemensCoefficient;
if (!DoCommonElectrocutionAttempt(targetUid, uid, ref siemens) || siemens <= 0)
@ -488,15 +488,4 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
}
_audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume));
}
public void SetElectrifiedWireCut(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.IsWireCut == value)
{
return;
}
ent.Comp.IsWireCut = value;
Dirty(ent);
}
}

View file

@ -1,7 +1,9 @@
using Content.Server.Popups;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Robust.Shared.Console;
using Content.Server.GameTicking;
namespace Content.Server.Ghost
{
@ -23,6 +25,14 @@ namespace Content.Server.Ghost
return;
}
var gameTicker = _entities.System<GameTicker>();
if (!gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
playerStatus is not PlayerGameStatus.JoinedGame)
{
shell.WriteLine("ghost-command-error-lobby");
return;
}
if (player.AttachedEntity is { Valid: true } frozen &&
_entities.HasComponent<AdminFrozenComponent>(frozen))
{

View file

@ -18,8 +18,11 @@ public sealed partial class CableComponent : Component
[DataField]
public EntProtoId CableDroppedOnCutPrototype = "CableHVStack1";
/// <summary>
/// The tool quality needed to cut the cable. Setting to null prevents cutting.
/// </summary>
[DataField]
public ProtoId<ToolQualityPrototype> CuttingQuality = SharedToolSystem.CutQuality;
public ProtoId<ToolQualityPrototype>? CuttingQuality = SharedToolSystem.CutQuality;
/// <summary>
/// Checked by <see cref="CablePlacerComponent"/> to determine if there is

View file

@ -35,7 +35,10 @@ public sealed partial class CableSystem : EntitySystem
if (args.Handled)
return;
args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, cable.CuttingDelay, cable.CuttingQuality, new CableCuttingFinishedEvent());
if (cable.CuttingQuality != null)
{
args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, cable.CuttingDelay, cable.CuttingQuality, new CableCuttingFinishedEvent());
}
}
private void OnCableCut(EntityUid uid, CableComponent cable, DoAfterEvent args)

View file

@ -17,7 +17,7 @@ public sealed partial class PowerWireAction : BaseWireAction
[DataField("pulseTimeout")]
private int _pulseTimeout = 30;
private ElectrocutionSystem _electrocutionSystem = default!;
private ElectrocutionSystem _electrocution = default!;
public override object StatusKey { get; } = PowerWireActionKey.Status;
@ -105,8 +105,8 @@ public sealed partial class PowerWireAction : BaseWireAction
&& !EntityManager.TryGetComponent(used, out electrified))
return;
_electrocutionSystem.SetElectrifiedWireCut((used, electrified), setting);
electrified.Enabled = setting;
_electrocution.SetElectrifiedWireCut((used, electrified), setting);
_electrocution.SetElectrified((used, electrified), setting);
}
/// <returns>false if failed, true otherwise, or if the entity cannot be electrified</returns>
@ -120,7 +120,7 @@ public sealed partial class PowerWireAction : BaseWireAction
// always set this to true
SetElectrified(wire.Owner, true, electrified);
var electrifiedAttempt = _electrocutionSystem.TryDoElectrifiedAct(wire.Owner, user);
var electrifiedAttempt = _electrocution.TryDoElectrifiedAct(wire.Owner, user);
// if we were electrified, then return false
return !electrifiedAttempt;
@ -161,7 +161,7 @@ public sealed partial class PowerWireAction : BaseWireAction
{
base.Initialize();
_electrocutionSystem = EntityManager.System<ElectrocutionSystem>();
_electrocution = EntityManager.System<ElectrocutionSystem>();
}
// This should add a wire into the entity's state, whether it be

View file

@ -20,7 +20,11 @@ public sealed partial class DungeonJob
}
var tileDef = _prototype.Index(tileProto);
data.SpawnGroups.TryGetValue(DungeonDataKey.WallMounts, out var spawnProto);
if (!data.SpawnGroups.TryGetValue(DungeonDataKey.WallMounts, out var spawnProto))
{
// caves can have no walls
return;
}
var checkedTiles = new HashSet<Vector2i>();
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);

View file

@ -13,11 +13,12 @@ public sealed class ServerInfoManager
private static readonly (CVarDef<string> cVar, string icon, string name)[] Vars =
{
// @formatter:off
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
(CCVars.InfoLinksGithub, "github", "info-link-github"),
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki")
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
(CCVars.InfoLinksGithub, "github", "info-link-github"),
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki"),
(CCVars.InfoLinksTelegram, "telegram", "info-link-telegram")
// @formatter:on
};

View file

@ -234,12 +234,6 @@ public sealed partial class ShuttleSystem
if (TryComp<PhysicsComponent>(shuttleUid, out var shuttlePhysics))
{
// Static physics type is set when station anchor is enabled
if (shuttlePhysics.BodyType == BodyType.Static)
{
reason = Loc.GetString("shuttle-console-static");
return false;
}
// Too large to FTL
if (FTLMassLimit > 0 && shuttlePhysics.Mass > FTLMassLimit)

View file

@ -0,0 +1,38 @@
using Content.Server.StationEvents.Events;
using Content.Shared.Access;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// Greytide Virus event specific configuration
/// </summary>
[RegisterComponent, Access(typeof(GreytideVirusRule))]
public sealed partial class GreytideVirusRuleComponent : Component
{
/// <summary>
/// Range from which the severity is randomly picked from.
/// </summary>
[DataField]
public MinMax SeverityRange = new(1, 3);
/// <summary>
/// Severity corresponding to the number of access groups affected.
/// Will pick randomly from the SeverityRange if not specified.
/// </summary>
[DataField]
public int? Severity;
/// <summary>
/// Access groups to pick from.
/// </summary>
[DataField]
public List<ProtoId<AccessGroupPrototype>> AccessGroups = new();
/// <summary>
/// Entities with this access level will be ignored.
/// </summary>
[DataField]
public List<ProtoId<AccessLevelPrototype>> Blacklist = new();
}

View file

@ -0,0 +1,96 @@
using Content.Server.StationEvents.Components;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Lock;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
/// <summary>
/// Greytide Virus event
/// This will open and bolt airlocks and unlock lockers from randomly selected access groups.
/// </summary>
public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComponent>
{
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly SharedDoorSystem _door = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Added(EntityUid uid, GreytideVirusRuleComponent virusComp, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
if (!TryComp<StationEventComponent>(uid, out var stationEvent))
return;
// pick severity randomly from range if not specified otherwise
virusComp.Severity ??= virusComp.SeverityRange.Next(_random);
virusComp.Severity = Math.Min(virusComp.Severity.Value, virusComp.AccessGroups.Count);
stationEvent.StartAnnouncement = Loc.GetString("station-event-greytide-virus-start-announcement", ("severity", virusComp.Severity.Value));
base.Added(uid, virusComp, gameRule, args);
}
protected override void Started(EntityUid uid, GreytideVirusRuleComponent virusComp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, virusComp, gameRule, args);
if (virusComp.Severity == null)
return;
// pick random access groups
var chosen = _random.GetItems(virusComp.AccessGroups, virusComp.Severity.Value, allowDuplicates: false);
// combine all the selected access groups
var accessIds = new HashSet<ProtoId<AccessLevelPrototype>>();
foreach (var group in chosen)
{
if (_prototype.TryIndex(group, out var proto))
accessIds.UnionWith(proto.Tags);
}
var firelockQuery = GetEntityQuery<FirelockComponent>();
var accessQuery = GetEntityQuery<AccessReaderComponent>();
var lockQuery = AllEntityQuery<LockComponent>();
while (lockQuery.MoveNext(out var lockUid, out var lockComp))
{
if (!accessQuery.TryComp(lockUid, out var accessComp))
continue;
// check access
// the AreAccessTagsAllowed function is a little weird because it technically has support for certain tags to be locked out of opening something
// which might have unintened side effects (see the comments in the function itself)
// but no one uses that yet, so it is fine for now
if (!_access.AreAccessTagsAllowed(accessIds, accessComp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessComp))
continue;
// open lockers
_lock.Unlock(lockUid, null, lockComp);
}
var airlockQuery = AllEntityQuery<AirlockComponent, DoorComponent>();
while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp))
{
// don't space everything
if (firelockQuery.HasComp(airlockUid))
continue;
// use the access reader from the door electronics if they exist
if (!_access.GetMainAccessReader(airlockUid, out var accessComp))
continue;
// check access
if (!_access.AreAccessTagsAllowed(accessIds, accessComp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessComp))
continue;
// open and bolt airlocks
_door.TryOpenAndBolt(airlockUid, doorComp, airlockComp);
}
}
}

View file

@ -38,6 +38,7 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
private const float WallVendEjectDistanceFromWall = 1f;
@ -334,6 +335,12 @@ namespace Content.Server.VendingMachines
finalState = VendingMachineVisualState.Off;
}
if (_light.TryGetLight(uid, out var pointlight))
{
var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
_light.SetEnabled(uid, lightState, pointlight);
}
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
}

View file

@ -22,13 +22,14 @@ public abstract partial class ComponentWireAction<TComponent> : BaseWireAction w
public override bool Cut(EntityUid user, Wire wire)
{
base.Cut(user, wire);
return EntityManager.TryGetComponent(wire.Owner, out TComponent? component) && Cut(user, wire, component);
// if the entity doesn't exist, we need to return true otherwise the wire sprite is never updated
return EntityManager.TryGetComponent(wire.Owner, out TComponent? component) ? Cut(user, wire, component) : true;
}
public override bool Mend(EntityUid user, Wire wire)
{
base.Mend(user, wire);
return EntityManager.TryGetComponent(wire.Owner, out TComponent? component) && Mend(user, wire, component);
return EntityManager.TryGetComponent(wire.Owner, out TComponent? component) ? Mend(user, wire, component) : true;
}
public override void Pulse(EntityUid user, Wire wire)

View file

@ -0,0 +1,64 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Database;
/// <summary>
/// Represents a raw HWID value together with its type.
/// </summary>
[Serializable]
public sealed class ImmutableTypedHwid(ImmutableArray<byte> hwid, HwidType type)
{
public readonly ImmutableArray<byte> Hwid = hwid;
public readonly HwidType Type = type;
public override string ToString()
{
var b64 = Convert.ToBase64String(Hwid.AsSpan());
return Type == HwidType.Modern ? $"V2-{b64}" : b64;
}
public static bool TryParse(string value, [NotNullWhen(true)] out ImmutableTypedHwid? hwid)
{
var type = HwidType.Legacy;
if (value.StartsWith("V2-", StringComparison.Ordinal))
{
value = value["V2-".Length..];
type = HwidType.Modern;
}
var array = new byte[GetBase64ByteLength(value)];
if (!Convert.TryFromBase64String(value, array, out _))
{
hwid = null;
return false;
}
// ReSharper disable once UseCollectionExpression
// Do not use collection expression, C# compiler is weird and it fails sandbox.
hwid = new ImmutableTypedHwid(ImmutableArray.Create(array), type);
return true;
}
private static int GetBase64ByteLength(string value)
{
// Why is .NET like this man wtf.
return 3 * (value.Length / 4) - value.TakeLast(2).Count(c => c == '=');
}
}
/// <summary>
/// Represents different types of HWIDs as exposed by the engine.
/// </summary>
public enum HwidType
{
/// <summary>
/// The legacy HWID system. Should only be used for checking old existing database bans.
/// </summary>
Legacy = 0,
/// <summary>
/// The modern HWID system.
/// </summary>
Modern = 1,
}

View file

@ -25,7 +25,7 @@ public static class BanPanelEuiStateMsg
{
public string? Player { get; set; }
public string? IpAddress { get; set; }
public byte[]? Hwid { get; set; }
public ImmutableTypedHwid? Hwid { get; set; }
public uint Minutes { get; set; }
public string Reason { get; set; }
public NoteSeverity Severity { get; set; }
@ -34,7 +34,7 @@ public static class BanPanelEuiStateMsg
public bool UseLastHwid { get; set; }
public bool Erase { get; set; }
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, byte[]? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
{
Player = player;
IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}";

View file

@ -51,4 +51,10 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<string> InfoLinksAppeal =
CVarDef.Create("infolinks.appeal", "", CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Link to Telegram channel to show in the launcher.
/// </summary>
public static readonly CVarDef<string> InfoLinksTelegram =
CVarDef.Create("infolinks.telegram", "", CVar.SERVER | CVar.REPLICATED);
}

View file

@ -4,6 +4,12 @@ namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
/// <summary>
/// Delay for auto-orientation. Used for people arriving via arrivals.
/// </summary>
public static readonly CVarDef<double> AutoOrientDelay =
CVarDef.Create("shuttle.auto_orient_delay", 2.0, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// If true then the camera will match the grid / map and is unchangeable.
/// - When traversing grids it will snap to 0 degrees rotation.

View file

@ -151,6 +151,10 @@ public sealed partial class ClimbSystem : VirtualController
if (args.Handled)
return;
// If already climbing then don't show outlines.
if (TryComp(args.Dragged, out ClimbingComponent? climbing) && climbing.IsClimbing)
return;
var canVault = args.User == args.Dragged
? CanVault(component, args.User, uid, out _)
: CanVault(component, args.User, args.Dragged, uid, out _);

View file

@ -66,7 +66,7 @@ public sealed partial class DoorComponent : Component
/// <summary>
/// When the door is active, this is the time when the state will next update.
/// </summary>
[AutoNetworkedField]
[AutoNetworkedField, ViewVariables]
public TimeSpan? NextStateChange;
/// <summary>

View file

@ -15,6 +15,19 @@ namespace Content.Shared.Doors
}
}
/// <summary>
/// Raised when the door's bolt status was changed.
/// </summary>
public sealed class DoorBoltsChangedEvent : EntityEventArgs
{
public readonly bool BoltsDown;
public DoorBoltsChangedEvent(bool boltsDown)
{
BoltsDown = boltsDown;
}
}
/// <summary>
/// Raised when the door is determining whether it is able to open.
/// Cancel to stop the door from being opened.

View file

@ -22,6 +22,7 @@ public abstract class SharedAirlockSystem : EntitySystem
SubscribeLocalEvent<AirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
SubscribeLocalEvent<AirlockComponent, DoorBoltsChangedEvent>(OnBoltsChanged);
SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<AirlockComponent, GetPryTimeModifierEvent>(OnGetPryMod);
@ -70,6 +71,13 @@ public abstract class SharedAirlockSystem : EntitySystem
}
}
private void OnBoltsChanged(EntityUid uid, AirlockComponent component, DoorBoltsChangedEvent args)
{
// If unbolted, reset the auto close timer
if (!args.BoltsDown)
UpdateAutoClose(uid, component);
}
private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
{
if (!CanChangeState(uid, component))
@ -145,7 +153,7 @@ public abstract class SharedAirlockSystem : EntitySystem
ent.Comp.EmergencyAccess = value;
Dirty(ent, ent.Comp); // This only runs on the server apparently so we need this.
UpdateEmergencyLightStatus(ent, ent.Comp);
var sound = ent.Comp.EmergencyAccess ? ent.Comp.EmergencyOnSound : ent.Comp.EmergencyOffSound;
if (predicted)
Audio.PlayPredicted(sound, ent, user: user);

View file

@ -96,6 +96,10 @@ public abstract partial class SharedDoorSystem
Dirty(ent, ent.Comp);
UpdateBoltLightStatus(ent);
// used to reset the auto-close timer after unbolting
var ev = new DoorBoltsChangedEvent(value);
RaiseLocalEvent(ent.Owner, ev);
var sound = value ? ent.Comp.BoltDownSound : ent.Comp.BoltUpSound;
if (predicted)
Audio.PlayPredicted(sound, ent, user: user);

View file

@ -400,6 +400,25 @@ public abstract partial class SharedDoorSystem : EntitySystem
Dirty(uid, door);
}
/// <summary>
/// Opens and then bolts a door.
/// Different from emagging this does not remove the access reader, so it can be repaired by simply unbolting the door.
/// </summary>
public bool TryOpenAndBolt(EntityUid uid, DoorComponent? door = null, AirlockComponent? airlock = null)
{
if (!Resolve(uid, ref door, ref airlock))
return false;
if (IsBolted(uid) || !airlock.Powered || door.State != DoorState.Closed)
{
return false;
}
SetState(uid, DoorState.Emagging, door);
return true;
}
#endregion
#region Closing
@ -465,17 +484,17 @@ public abstract partial class SharedDoorSystem : EntitySystem
if (!Resolve(uid, ref door, ref physics))
return false;
door.Partial = true;
// Make sure no entity walked into the airlock when it started closing.
if (!CanClose(uid, door))
{
door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo;
door.State = DoorState.Opening;
AppearanceSystem.SetData(uid, DoorVisuals.State, DoorState.Opening);
door.State = DoorState.Open;
AppearanceSystem.SetData(uid, DoorVisuals.State, DoorState.Open);
Dirty(uid, door);
return false;
}
door.Partial = true;
SetCollidable(uid, true, door, physics);
door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
Dirty(uid, door);
@ -709,6 +728,8 @@ public abstract partial class SharedDoorSystem : EntitySystem
}
door.NextStateChange = GameTiming.CurTime + delay.Value;
Dirty(uid, door);
_activeDoors.Add((uid, door));
}

View file

@ -74,33 +74,38 @@ namespace Content.Shared.DrawDepth
/// </summary>
Items = DrawDepthTag.Default + 3,
Mobs = DrawDepthTag.Default + 4,
OverMobs = DrawDepthTag.Default + 5,
/// <summary>
/// Stuff that should be drawn below mobs, but on top of items. Like muzzle flash.
/// </summary>
BelowMobs = DrawDepthTag.Default + 4,
Doors = DrawDepthTag.Default + 6,
Mobs = DrawDepthTag.Default + 5,
OverMobs = DrawDepthTag.Default + 6,
Doors = DrawDepthTag.Default + 7,
/// <summary>
/// Blast doors and shutters which go over the usual doors.
/// </summary>
BlastDoors = DrawDepthTag.Default + 7,
BlastDoors = DrawDepthTag.Default + 8,
/// <summary>
/// Stuff that needs to draw over most things, but not effects, like Kudzu.
/// </summary>
Overdoors = DrawDepthTag.Default + 8,
Overdoors = DrawDepthTag.Default + 9,
/// <summary>
/// Explosions, fire, melee swings. Whatever.
/// </summary>
Effects = DrawDepthTag.Default + 9,
Effects = DrawDepthTag.Default + 10,
Ghosts = DrawDepthTag.Default + 10,
Ghosts = DrawDepthTag.Default + 11,
/// <summary>
/// Use this selectively if it absolutely needs to be drawn above (almost) everything else. Examples include
/// the pointing arrow, the drag & drop ghost-entity, and some debug tools.
/// </summary>
Overlays = DrawDepthTag.Default + 11,
Overlays = DrawDepthTag.Default + 12,
}
}

View file

@ -0,0 +1,7 @@
namespace Content.Shared.Electrocution;
/// <summary>
/// Handles toggling sprite layers for the electrocution HUD to show if an entity with the ElectrifiedComponent is electrified.
/// </summary>
[RegisterComponent]
public sealed partial class ElectrocutionHUDVisualsComponent : Component;

View file

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Electrocution;
/// <summary>
/// Allow an entity to see the Electrocution HUD showing electrocuted doors.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ShowElectrocutionHUDComponent : Component;

View file

@ -5,11 +5,13 @@ namespace Content.Shared.Electrocution;
[Serializable, NetSerializable]
public enum ElectrifiedLayers : byte
{
Powered
Sparks,
HUD,
}
[Serializable, NetSerializable]
public enum ElectrifiedVisuals : byte
{
IsPowered
ShowSparks, // only shown when zapping someone, deactivated after a short time
IsElectrified, // if the entity is electrified or not, used for the AI HUD
}

View file

@ -5,6 +5,8 @@ namespace Content.Shared.Electrocution
{
public abstract class SharedElectrocutionSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
@ -35,6 +37,19 @@ namespace Content.Shared.Electrocution
ent.Comp.Enabled = value;
Dirty(ent, ent.Comp);
_appearance.SetData(ent.Owner, ElectrifiedVisuals.IsElectrified, value);
}
public void SetElectrifiedWireCut(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.IsWireCut == value)
{
return;
}
ent.Comp.IsWireCut = value;
Dirty(ent);
}
/// <param name="uid">Entity being electrocuted.</param>

View file

@ -5,8 +5,9 @@ namespace Content.Shared.Movement.Components;
/// <summary>
/// Automatically rotates eye upon grid traversals.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class AutoOrientComponent : Component
{
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan? NextChange;
}

View file

@ -0,0 +1,51 @@
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Shared.Movement.Systems;
public sealed class AutoOrientSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
private TimeSpan _delay = TimeSpan.Zero;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AutoOrientComponent, EntParentChangedMessage>(OnEntParentChanged);
Subs.CVar(_cfgManager, CCVars.AutoOrientDelay, OnAutoOrient, true);
}
private void OnAutoOrient(double obj)
{
_delay = TimeSpan.FromSeconds(obj);
}
private void OnEntParentChanged(Entity<AutoOrientComponent> ent, ref EntParentChangedMessage args)
{
ent.Comp.NextChange = _timing.CurTime + _delay;
Dirty(ent);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AutoOrientComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextChange <= _timing.CurTime)
{
comp.NextChange = null;
Dirty(uid, comp);
_mover.ResetCamera(uid);
}
}
}
}

View file

@ -57,8 +57,6 @@ namespace Content.Shared.Movement.Systems
SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnMoverHandleState);
SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(OnInputParentChange);
SubscribeLocalEvent<AutoOrientComponent, EntParentChangedMessage>(OnAutoParentChange);
SubscribeLocalEvent<FollowedComponent, EntParentChangedMessage>(OnFollowedParentChange);
Subs.CVar(_configManager, CCVars.CameraRotationLocked, obj => CameraRotationLocked = obj, true);
@ -146,11 +144,6 @@ namespace Content.Shared.Movement.Systems
protected virtual void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) {}
private void OnAutoParentChange(Entity<AutoOrientComponent> entity, ref EntParentChangedMessage args)
{
ResetCamera(entity.Owner);
}
public void RotateCamera(EntityUid uid, Angle angle)
{
if (CameraRotationLocked || !MoverQuery.TryGetComponent(uid, out var mover))

View file

@ -34,7 +34,7 @@ public abstract partial class SharedStationAiSystem
}
/// <summary>
/// Attempts to bolt door. If wire was cut (AI) or its not powered - notifies AI and does nothing.
/// Attempts to toggle the door's emergency access. If wire was cut (AI) or its not powered - notifies AI and does nothing.
/// </summary>
private void OnAirlockEmergencyAccess(EntityUid ent, AirlockComponent component, StationAiEmergencyAccessEvent args)
{
@ -48,7 +48,7 @@ public abstract partial class SharedStationAiSystem
}
/// <summary>
/// Attempts to bolt door. If wire was cut (AI or for one of power-wires) or its not powered - notifies AI and does nothing.
/// Attempts to electrify the door. If wire was cut (AI or for one of power-wires) or its not powered - notifies AI and does nothing.
/// </summary>
private void OnElectrified(EntityUid ent, ElectrifiedComponent component, StationAiElectrifiedEvent args)
{

View file

@ -394,6 +394,9 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != StationAiCoreComponent.Container)
return;
if (_timing.ApplyingState)
return;

View file

@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Storage;
@ -145,27 +146,23 @@ public abstract class SharedStationSpawningSystem : EntitySystem
if (startingGear.Storage.Count > 0)
{
var coords = _xformSystem.GetMapCoordinates(entity);
var ents = new ValueList<EntityUid>();
_inventoryQuery.TryComp(entity, out var inventoryComp);
foreach (var (slot, entProtos) in startingGear.Storage)
foreach (var (slotName, entProtos) in startingGear.Storage)
{
ents.Clear();
if (entProtos.Count == 0)
if (entProtos == null || entProtos.Count == 0)
continue;
if (inventoryComp != null &&
InventorySystem.TryGetSlotEntity(entity, slot, out var slotEnt, inventoryComponent: inventoryComp) &&
InventorySystem.TryGetSlotEntity(entity, slotName, out var slotEnt, inventoryComponent: inventoryComp) &&
_storageQuery.TryComp(slotEnt, out var storage))
{
foreach (var ent in entProtos)
{
ents.Add(Spawn(ent, coords));
}
foreach (var ent in ents)
foreach (var entProto in entProtos)
{
_storage.Insert(slotEnt.Value, ent, out _, storageComp: storage, playSound: false);
var spawnedEntity = Spawn(entProto, coords);
_storage.Insert(slotEnt.Value, spawnedEntity, out _, storageComp: storage, playSound: false);
}
}
}

View file

@ -1,154 +1,4 @@
Entries:
- author: EmoGarbage404
changes:
- message: Legends tell of horrifying Goliaths that roam the mining asteroid.
type: Add
id: 7137
time: '2024-08-18T16:22:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30839
- author: Beck Thompson
changes:
- message: Cutting food now moves the sliced pieces a small amount!
type: Tweak
id: 7138
time: '2024-08-18T21:18:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31166
- author: lzk228
changes:
- message: Pizza and pizza box now have 3x2 size in inventory.
type: Tweak
- message: Pizze box is 4x2 inside and always will have a pizza with a knife inside.
type: Tweak
- message: Pizza box have whitelist for utensils and pizza.
type: Tweak
id: 7139
time: '2024-08-18T21:55:42.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31171
- author: Deatherd
changes:
- message: Sharks Go RAWR!
type: Tweak
id: 7140
time: '2024-08-18T22:18:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31142
- author: slarticodefast
changes:
- message: Fixed the radiation collector warning light thresholds.
type: Fix
id: 7141
time: '2024-08-18T22:25:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31175
- author: Potato1234_x
changes:
- message: Added tea plants that can be dried and ground to make tea powder which
can then be used to make tea.
type: Add
- message: Added blue pumpkins. Currently useless but recipe uses are coming soon.
type: Add
id: 7142
time: '2024-08-18T22:28:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25092
- author: deltanedas
changes:
- message: Added Memory Cells for storing logic signals persistently.
type: Add
id: 7143
time: '2024-08-18T22:34:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/24983
- author: Psychpsyo
changes:
- message: The random sentience event is back and can no longer pick things that
aren't even on the station.
type: Add
id: 7144
time: '2024-08-18T23:41:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29123
- author: EmoGarbage404
changes:
- message: Ore can no longer be destroyed by explosions. Happy blast mining.
type: Tweak
id: 7145
time: '2024-08-19T01:55:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31182
- author: slarticodefast
changes:
- message: Mobs without hands can no longer toggle other players' suit pieces.
type: Fix
id: 7146
time: '2024-08-19T02:41:27.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31152
- author: Goldminermac
changes:
- message: Chocolate-chip and blueberry pancakes can now be in stacks of up to nine
for consistency.
type: Fix
id: 7147
time: '2024-08-19T02:42:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31123
- author: tosatur
changes:
- message: Made hydroponics alert light more orange
type: Tweak
id: 7148
time: '2024-08-19T02:48:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31088
- author: redmushie
changes:
- message: News management console now checks for Service ID card access instead
of the manifest
type: Fix
id: 7149
time: '2024-08-19T02:55:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31160
- author: Moomoobeef
changes:
- message: Added pitchers for the chef who wants to serve beverages too.
type: Add
id: 7150
time: '2024-08-19T03:01:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31105
- author: EmoGarbage404
changes:
- message: Space carp and Sharkminnows now drop teeth when butchered.
type: Add
- message: Added new bounties for carp and shark teeth.
type: Add
id: 7151
time: '2024-08-19T03:04:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31070
- author: to4no_fix
changes:
- message: Now it takes 5 seconds to take off or put on a muzzle
type: Tweak
- message: Now it takes 5 seconds to take off or put on a blindfold
type: Tweak
- message: Added a recipe for producing a straitjacket, it opens when researching
the Special Means technology, it can be produced at the security techfab
type: Add
id: 7152
time: '2024-08-19T03:05:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31095
- author: Magicalus
changes:
- message: Suit sensors, borgs, and PDAs can no longer be saved to device-lists.
type: Tweak
id: 7153
time: '2024-08-19T03:13:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30997
- author: UBlueberry
changes:
- message: The guidebook entries for all antagonists have been revised.
type: Tweak
id: 7154
time: '2024-08-19T03:16:05.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31075
- author: TheWaffleJesus
changes:
- message: ERT Chaplains now have blessings to use their bible.
type: Fix
id: 7155
time: '2024-08-19T03:19:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30993
- author: DieselMohawk
changes:
- message: Reshaped the Security Helmet
@ -3933,3 +3783,146 @@
id: 7636
time: '2024-11-22T02:56:05.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31076
- author: chromiumboy
changes:
- message: Added the gas pipe sensor. These sensors monitor the mixture of gases
passing through their pipe sub-network and report this information to any connected
air alarms
type: Add
id: 7637
time: '2024-11-22T03:46:10.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33128
- author: IProduceWidgets
changes:
- message: The terminal is more tamper proof.
type: Fix
id: 7638
time: '2024-11-22T22:50:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33281
- author: MissKay1994
changes:
- message: The salvage vendor now has enough equipment for everyone
type: Tweak
id: 7639
time: '2024-11-23T02:53:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33437
- author: slarticodefast
changes:
- message: The AI and observers can now see if doors are electrified.
type: Add
id: 7640
time: '2024-11-23T06:37:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33466
- author: Winkarst-cpu
changes:
- message: Now submit button in admin notes becomes disabled on switching type back
to note.
type: Fix
id: 7641
time: '2024-11-23T06:41:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33456
- author: metalgearsloth
changes:
- message: Fix airlock animations mispredicting if the closing animation is interrupted,
e.g. via walking into it.
type: Fix
id: 7642
time: '2024-11-23T09:31:08.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33481
- author: metalgearsloth
changes:
- message: Drag-drop outline no longer shows the vaulting outlines if you're vaulting.
type: Tweak
id: 7643
time: '2024-11-23T11:19:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33477
- author: goet
changes:
- message: Useless wires some vending machines have can be cut now.
type: Fix
id: 7644
time: '2024-11-23T11:41:37.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32447
- author: IProduceWidgets
changes:
- message: Arrivals shuttle is more tamper proof.
type: Fix
id: 7645
time: '2024-11-23T15:14:13.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33284
- author: metalgearsloth
changes:
- message: The auto-orientation when showing up on the arrivals shuttle now has
a delay to it.
type: Tweak
id: 7646
time: '2024-11-23T16:52:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33479
- author: Winkarst-cpu
changes:
- message: Now muzzle flashes are displayed below mobs.
type: Tweak
id: 7647
time: '2024-11-24T04:20:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33465
- author: MilenVolf
changes:
- message: Expeditions can now be completed prematurely again by making an FTL jump.
type: Fix
id: 7648
time: '2024-11-24T08:11:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33507
- author: deltanedas
changes:
- message: Fixed expeditions on cave planets not having any ore.
type: Fix
id: 7649
time: '2024-11-24T08:49:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32890
- author: slarticodefast
changes:
- message: Fixed doors not auto-closing correctly after being unbolted in an open
state.
type: Fix
id: 7650
time: '2024-11-25T04:26:54.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33524
- author: Schrodinger71
changes:
- message: Fixed a bug letting players type "ghost" in the console and then see
the whole chat while being in the lobby.
type: Fix
id: 7651
time: '2024-11-25T07:20:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33529
- author: Minemoder
changes:
- message: The shark plushie now goes rawr when hitting something or being thrown.
type: Tweak
id: 7652
time: '2024-11-25T12:23:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33540
- author: red15
changes:
- message: Vending machine lights turns off when broken.
type: Fix
id: 7653
time: '2024-11-25T12:35:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33513
- author: metalgearsloth
changes:
- message: Adjusted the top menu on the separated game screen. The buttons will
now form multiple rows and no longer overflow into the viewport.
type: Tweak
id: 7654
time: '2024-11-26T00:59:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33047
- author: slarticodefast
changes:
- message: Added the greytide virus station event. It will bolt open all doors in
a few randomly chosen departments and unlock lockers with the corresponding
access.
type: Add
id: 7655
time: '2024-11-26T13:50:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33547

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
gas-pipe-sensor-distribution-loop = Distribution loop
gas-pipe-sensor-waste-loop = Waste loop
gas-pipe-sensor-mixed-air = Mixed air
gas-pipe-sensor-teg-hot-loop = TEG hot loop
gas-pipe-sensor-teg-cold-loop = TEG cold loop

View file

@ -3,3 +3,4 @@ ghost-command-help-text = The ghost command turns you into a ghost and makes the
Please note that you cannot return to your character's body after ghosting.
ghost-command-no-session = You have no session, you can't ghost.
ghost-command-denied = You cannot ghost right now.
ghost-command-error-lobby = You can't ghost right now. You are not in the game!

View file

@ -4,5 +4,6 @@ server-info-discord-button = Discord
server-info-website-button = Website
server-info-wiki-button = Wiki
server-info-forum-button = Forum
server-info-telegram-button = Telegram
server-info-report-button = Report Bugs
server-info-credits-button = Credits

View file

@ -5,3 +5,4 @@ info-link-forum = Forum
info-link-github = GitHub
info-link-website = Website
info-link-wiki = Wiki
info-link-telegram = Telegram

View file

@ -4,7 +4,6 @@ shuttle-pilot-end = Stopped piloting
shuttle-console-in-ftl = Currently in FTL
shuttle-console-mass = Too large to FTL
shuttle-console-prevent = You are unable to pilot this ship
shuttle-console-static = Grid is static
# NAV

View file

@ -0,0 +1 @@
station-event-greytide-virus-start-announcement = Gr3y.T1d3 virus detected in the station's secure locking encryption subroutines. Severity level of { $severity }. Recommend station AI involvement.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -154,7 +154,7 @@
sprite: Objects/Specific/Service/vending_machine_restock.rsi
state: base
product: CrateVendingMachineRestockSalvageEquipmentFilled
cost: 1000
cost: 1500
category: cargoproduct-category-name-engineering
group: market

View file

@ -10,7 +10,6 @@
- id: ClothingBeltSecurityFilled
- id: Flash
- id: ClothingEyesGlassesSecurity
- id: ClothingHeadsetAltSecurity
- id: ClothingHandsGlovesCombat
- id: ClothingShoesBootsJack
- id: ClothingOuterCoatWarden
@ -40,7 +39,6 @@
- id: ClothingBeltSecurityFilled
- id: Flash
- id: ClothingEyesGlassesSecurity
- id: ClothingHeadsetAltSecurity
- id: ClothingHandsGlovesCombat
- id: ClothingShoesBootsJack
- id: ClothingOuterCoatWarden

View file

@ -3,10 +3,10 @@
startingInventory:
Crowbar: 2
Pickaxe: 4
OreBag: 2
OreBag: 4
Flare: 4
FlashlightLantern: 2
HandheldGPSBasic: 2
RadioHandheld: 2
WeaponGrapplingGun: 2
WeaponGrapplingGun: 4
WeaponProtoKineticAccelerator: 4

View file

@ -55,6 +55,7 @@
skipChecks: true
- type: Ghost
- type: GhostHearing
- type: ShowElectrocutionHUD
- type: IntrinsicRadioReceiver
- type: ActiveRadio
receiveAllChannels: true

View file

@ -32,6 +32,7 @@
- type: IgnoreUIRange
- type: StationAiHeld
- type: StationAiOverlay
- type: ShowElectrocutionHUD
- type: ActionGrant
actions:
- ActionJumpToCore

View file

@ -498,12 +498,22 @@
- type: Sprite
sprite: Objects/Fun/sharkplush.rsi
state: blue
- type: EmitSoundOnLand
sound:
path: /Audio/Items/Toys/rawr.ogg
- type: EmitSoundOnTrigger
sound:
path: /Audio/Items/Toys/rawr.ogg
- type: EmitSoundOnUse
sound:
path: /Audio/Items/Toys/rawr.ogg
- type: EmitSoundOnActivate
sound:
path: /Audio/Items/Toys/rawr.ogg
- type: MeleeWeapon
wideAnimationRotation: 180
soundHit:
path: /Audio/Items/Toys/rawr.ogg
- type: Item
heldPrefix: blue
storedRotation: -90

View file

@ -55,6 +55,8 @@
reagents:
- ReagentId: Cellulose
Quantity: 6
- type: Item
heldPrefix: cardboard
- type: entity
parent: MaterialCardboard
@ -136,6 +138,8 @@
- type: Construction
graph: WebObjects # not sure if I should either keep this here or just make another prototype. Will keep it here just in case.
node: cloth
- type: Item
heldPrefix: cloth
- type: entity
parent: MaterialCloth
@ -196,6 +200,8 @@
tags:
- ClothMade
- RawMaterial
- type: Item
heldPrefix: durathread
- type: entity
parent: MaterialDurathread
@ -335,8 +341,7 @@
sprite: Objects/Materials/materials.rsi
state: corgihide
- type: Item
sprite: Clothing/Head/Misc/hides.rsi
heldPrefix: corgi
heldPrefix: corgihide
- type: Clothing
sprite: Clothing/Head/Misc/hides.rsi
equippedPrefix: corgi2
@ -428,6 +433,8 @@
tags:
- ClothMade
- RawMaterial
- type: Item
heldPrefix: cotton
- type: entity
parent: MaterialCotton
@ -480,6 +487,8 @@
tags:
- ClothMade
- RawMaterial
- type: Item
heldPrefix: pyrotton
- type: entity
parent: MaterialPyrotton
@ -539,6 +548,8 @@
- ReagentId: Honk
Quantity: 5
- type: Appearance
- type: Item
heldPrefix: bananium
- type: entity
parent: MaterialBananium
@ -589,6 +600,9 @@
tags:
- ClothMade
- RawMaterial
- type: Item
sprite: Objects/Materials/silk.rsi
heldPrefix: silk
- type: entity
parent: MaterialWebSilk
@ -711,6 +725,8 @@
reagents:
- ReagentId: Vitamin
Quantity: 3
- type: Item
heldPrefix: bones
- type: entity
parent: MaterialBones
@ -765,6 +781,8 @@
- goliath_hide_3
- type: Item
size: Large
heldPrefix: goliathhide
sprite: Objects/Materials/hide.rsi
shape:
- 0,0,2,2

View file

@ -46,6 +46,8 @@
reagents:
- ReagentId: Gold
Quantity: 10
- type: Item
heldPrefix: gold
- type: entity
parent: GoldOre
@ -77,6 +79,8 @@
reagents:
- ReagentId: Carbon
Quantity: 20
- type: Item
heldPrefix: diamond
- type: entity
parent: DiamondOre
@ -108,6 +112,8 @@
reagents:
- ReagentId: Iron
Quantity: 10
- type: Item
heldPrefix: iron
- type: entity
id: SteelOre1
@ -144,6 +150,8 @@
reagents:
- ReagentId: Plasma
Quantity: 10
- type: Item
heldPrefix: plasma
- type: entity
parent: PlasmaOre
@ -175,6 +183,8 @@
reagents:
- ReagentId: Silver
Quantity: 10
- type: Item
heldPrefix: silver
- type: entity
parent: SilverOre
@ -206,6 +216,8 @@
reagents:
- ReagentId: Silicon
Quantity: 10
- type: Item
heldPrefix: spacequartz
- type: entity
parent: SpaceQuartz
@ -245,6 +257,8 @@
- ReagentId: Radium
Quantity: 2
canReact: false
- type: Item
heldPrefix: uranium
- type: entity
parent: UraniumOre
@ -285,6 +299,8 @@
Quantity: 2
- ReagentId: Honk
Quantity: 5
- type: Item
heldPrefix: bananium
- type: entity
parent: BananiumOre
@ -324,6 +340,8 @@
- type: PhysicalComposition
materialComposition:
Coal: 100
- type: Item
heldPrefix: coal
- type: entity
parent: Coal
@ -381,6 +399,8 @@
Quantity: 10
- ReagentId: Iodine
Quantity: 5
- type: Item
heldPrefix: salt
- type: entity
parent: SaltOre

View file

@ -50,7 +50,7 @@
#Sunrise-end
- type: Item
size: Normal
# heldPrefix: rods
heldPrefix: rods
- type: Construction
graph: MetalRod
node: MetalRod

View file

@ -5,7 +5,7 @@
- type: TimedDespawn
lifetime: 0.4
- type: Sprite
drawdepth: Effects
drawdepth: BelowMobs
layers:
- shader: unshaded
map: ["enum.EffectLayers.Unshaded"]
@ -315,9 +315,11 @@
- EmitterBolt
- type: TimedDespawn
lifetime: 3
# Sunrise-Start
- type: Reflective
reflective:
- Energy
# Sunrise-End
- type: entity
name: watcher bolt
@ -703,33 +705,6 @@
- type: StaminaDamageOnCollide
damage: 80
# Sunrise-Start
- type: entity
id: BaseBulletGrenade
parent: BaseItem
categories: [ HideSpawnMenu ]
components:
- type: Projectile
damage:
types:
Blunt: 5
deleteOnCollide: false
- type: StartTimerOnShoot
- type: OnUseTimerTrigger
delay: 2
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
density: 50
mask:
- ItemMask
restitution: 0.05
friction: 0.5
# Sunrise-End
- type: entity
id: BulletGrenadeBlast
name: blast grenade
@ -1083,6 +1058,7 @@
proto: BulletDisablerSmg
count: 3 #bit stronger than a disabler if you hit your shots you goober, still not a 2 hit stun though
spread: 9
# SUNRISE
- type: entity
@ -1211,7 +1187,6 @@
soundHit:
collection: WeakHit
# Sunrise-AEG
- type: entity
name: energy bolt
id: BulletEnergyGunLaser
@ -1252,7 +1227,6 @@
soundHit:
collection: WeakHit
# Sunrise-NT
- type: entity
id: BulletRocketNT
name: rocket
@ -1274,3 +1248,28 @@
radius: 3.5
color: orange
energy: 0.5
- type: entity
id: BaseBulletGrenade
parent: BaseItem
categories: [ HideSpawnMenu ]
components:
- type: Projectile
damage:
types:
Blunt: 5
deleteOnCollide: false
- type: StartTimerOnShoot
- type: OnUseTimerTrigger
delay: 2
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
density: 50
mask:
- ItemMask
restitution: 0.05
friction: 0.5

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