diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 16cb5017d6a154..d08ae1350208f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,21 +41,10 @@ jobs: - name: Package client run: dotnet run --project Content.Packaging client --no-wipe-release - - name: Upload build artifact - id: artifact-upload-step - uses: actions/upload-artifact@v4 - with: - name: build - path: release/*.zip - compression-level: 0 - retention-days: 0 - - name: Publish version - run: Tools/publish_github_artifact.py + run: Tools/publish_multi_request.py env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - ARTIFACT_ID: ${{ steps.artifact-upload-step.outputs.artifact-id }} GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }} - name: Publish changelog (Discord) @@ -68,8 +57,3 @@ jobs: run: Tools/actions_changelog_rss.py env: CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }} - - - uses: geekyeggo/delete-artifact@v5 - if: always() - with: - name: build diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs index f805f6643d2fad..2425cdefb916cc 100644 --- a/Content.Client/Actions/UI/ActionAlertTooltip.cs +++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs @@ -101,7 +101,7 @@ protected override void FrameUpdate(FrameEventArgs args) { var duration = Cooldown.Value.End - Cooldown.Value.Start; - if (!FormattedMessage.TryFromMarkup($"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]", out var markup)) + if (!FormattedMessage.TryFromMarkup(Loc.GetString("ui-actionslot-duration", ("duration", (int)duration.TotalSeconds), ("timeLeft", (int)timeLeft.TotalSeconds + 1)), out var markup)) return; _cooldownLabel.SetMessage(markup); diff --git a/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs b/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs index 7cb32b43df59f3..615f1434df229d 100644 --- a/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs +++ b/Content.Client/Administration/UI/SetOutfit/SetOutfitMenu.xaml.cs @@ -1,14 +1,13 @@ +using System.Linq; using System.Numerics; using Content.Client.UserInterface.Controls; +using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Robust.Client.AutoGenerated; using Robust.Client.Console; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Prototypes; namespace Content.Client.Administration.UI.SetOutfit @@ -65,9 +64,18 @@ private void SearchBarOnOnTextChanged(LineEdit.LineEditEventArgs obj) PopulateByFilter(SearchBar.Text); } + private IEnumerable GetPrototypes() + { + // Filter out any StartingGearPrototypes that belong to loadouts + var loadouts = _prototypeManager.EnumeratePrototypes(); + var loadoutGears = loadouts.Select(l => l.StartingGear); + return _prototypeManager.EnumeratePrototypes() + .Where(p => !loadoutGears.Contains(p.ID)); + } + private void PopulateList() { - foreach (var gear in _prototypeManager.EnumeratePrototypes()) + foreach (var gear in GetPrototypes()) { OutfitList.Add(GetItem(gear, OutfitList)); } @@ -76,7 +84,7 @@ private void PopulateList() private void PopulateByFilter(string filter) { OutfitList.Clear(); - foreach (var gear in _prototypeManager.EnumeratePrototypes()) + foreach (var gear in GetPrototypes()) { if (!string.IsNullOrEmpty(filter) && gear.ID.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant())) diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs new file mode 100644 index 00000000000000..3c97b8b37d1545 --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs @@ -0,0 +1,30 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.CartridgeLoader.Cartridges; + +public sealed partial class WantedListUi : UIFragment +{ + private WantedListUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new WantedListUiFragment(); + } + + public override void UpdateState(BoundUserInterfaceState state) + { + switch (state) + { + case WantedListUiState cast: + _fragment?.UpdateState(cast.Records); + break; + } + } +} diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs new file mode 100644 index 00000000000000..4137f6c2af0cc0 --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs @@ -0,0 +1,240 @@ +using System.Linq; +using Content.Client.UserInterface.Controls; +using Content.Shared.CriminalRecords.Systems; +using Content.Shared.Security; +using Content.Shared.StatusIcon; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class WantedListUiFragment : BoxContainer +{ + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private readonly SpriteSystem _spriteSystem; + + private string? _selectedTargetName; + private List _wantedRecords = new(); + + public WantedListUiFragment() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _spriteSystem = _entitySystem.GetEntitySystem(); + + SearchBar.OnTextChanged += OnSearchBarTextChanged; + } + + private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) + { + var found = !String.IsNullOrWhiteSpace(args.Text) + ? _wantedRecords.FindAll(r => + r.TargetInfo.Name.Contains(args.Text) || + r.Status.ToString().Contains(args.Text, StringComparison.OrdinalIgnoreCase)) + : _wantedRecords; + + UpdateState(found, false); + } + + public void UpdateState(List records, bool refresh = true) + { + if (records.Count == 0) + { + NoRecords.Visible = true; + RecordsList.Visible = false; + RecordUnselected.Visible = false; + PersonContainer.Visible = false; + + _selectedTargetName = null; + if (refresh) + _wantedRecords.Clear(); + + RecordsList.PopulateList(new List()); + + return; + } + + NoRecords.Visible = false; + RecordsList.Visible = true; + RecordUnselected.Visible = true; + PersonContainer.Visible = false; + + var dataList = records.Select(r => new StatusListData(r)).ToList(); + + RecordsList.GenerateItem = GenerateItem; + RecordsList.ItemPressed = OnItemSelected; + RecordsList.PopulateList(dataList); + + if (refresh) + _wantedRecords = records; + } + + private void OnItemSelected(BaseButton.ButtonEventArgs args, ListData data) + { + if (data is not StatusListData(var record)) + return; + + FormattedMessage GetLoc(string fluentId, params (string,object)[] args) + { + var msg = new FormattedMessage(); + var fluent = Loc.GetString(fluentId, args); + msg.AddMarkupPermissive(fluent); + return msg; + } + + // Set personal info + PersonName.Text = record.TargetInfo.Name; + TargetAge.SetMessage(GetLoc( + "wanted-list-age-label", + ("age", record.TargetInfo.Age) + )); + TargetJob.SetMessage(GetLoc( + "wanted-list-job-label", + ("job", record.TargetInfo.JobTitle.ToLower()) + )); + TargetSpecies.SetMessage(GetLoc( + "wanted-list-species-label", + ("species", record.TargetInfo.Species.ToLower()) + )); + TargetGender.SetMessage(GetLoc( + "wanted-list-gender-label", + ("gender", record.TargetInfo.Gender) + )); + + // Set reason + WantedReason.SetMessage(GetLoc( + "wanted-list-reason-label", + ("reason", record.Reason ?? Loc.GetString("wanted-list-unknown-reason-label")) + )); + + // Set status + PersonState.SetMessage(GetLoc( + "wanted-list-status-label", + ("status", record.Status.ToString().ToLower()) + )); + + // Set initiator + InitiatorName.SetMessage(GetLoc( + "wanted-list-initiator-label", + ("initiator", record.Initiator ?? Loc.GetString("wanted-list-unknown-initiator-label")) + )); + + // History table + // Clear table if it exists + HistoryTable.RemoveAllChildren(); + + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-time-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + }); + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-reason-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + HorizontalExpand = true, + }); + + HistoryTable.AddChild(new Label() + { + Text = Loc.GetString("wanted-list-history-table-initiator-col"), + StyleClasses = { "LabelSmall" }, + HorizontalAlignment = HAlignment.Center, + }); + + if (record.History.Count > 0) + { + HistoryTable.Visible = true; + + foreach (var history in record.History.OrderByDescending(h => h.AddTime)) + { + HistoryTable.AddChild(new Label() + { + Text = $"{history.AddTime.Hours:00}:{history.AddTime.Minutes:00}:{history.AddTime.Seconds:00}", + StyleClasses = { "LabelSmall" }, + VerticalAlignment = VAlignment.Top, + }); + + HistoryTable.AddChild(new RichTextLabel() + { + Text = $"[color=white]{history.Crime}[/color]", + HorizontalExpand = true, + VerticalAlignment = VAlignment.Top, + StyleClasses = { "LabelSubText" }, + Margin = new(10f, 0f), + }); + + HistoryTable.AddChild(new RichTextLabel() + { + Text = $"[color=white]{history.InitiatorName}[/color]", + StyleClasses = { "LabelSubText" }, + VerticalAlignment = VAlignment.Top, + }); + } + } + + RecordUnselected.Visible = false; + PersonContainer.Visible = true; + + // Save selected item + _selectedTargetName = record.TargetInfo.Name; + } + + private void GenerateItem(ListData data, ListContainerButton button) + { + if (data is not StatusListData(var record)) + return; + + var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true }; + var label = new Label() { Text = record.TargetInfo.Name }; + var rect = new TextureRect() + { + TextureScale = new(2.2f), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Margin = new(0f, 0f, 6f, 0f), + }; + + if (record.Status is not SecurityStatus.None) + { + var proto = "SecurityIcon" + record.Status switch + { + SecurityStatus.Detained => "Incarcerated", + _ => record.Status.ToString(), + }; + + if (_prototypeManager.TryIndex(proto, out var prototype)) + { + rect.Texture = _spriteSystem.Frame0(prototype.Icon); + } + } + + box.AddChild(rect); + box.AddChild(label); + button.AddChild(box); + button.AddStyleClass(ListContainer.StyleClassListContainerButton); + + if (record.TargetInfo.Name.Equals(_selectedTargetName)) + { + button.Pressed = true; + // For some reason the event is not called when `Pressed` changed, call it manually. + OnItemSelected( + new(button, new(new(), BoundKeyState.Down, new(), false, new(), new())), + data); + } + } +} + +internal record StatusListData(WantedRecord Record) : ListData; diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml new file mode 100644 index 00000000000000..7b5d116ad74b8e --- /dev/null +++ b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml @@ -0,0 +1,50 @@ + + + + + + + diff --git a/Content.Client/Explosion/ExplosionSystem.cs b/Content.Client/Explosion/ExplosionSystem.cs index a2ed2d50e0d1ef..692782ded4b77b 100644 --- a/Content.Client/Explosion/ExplosionSystem.cs +++ b/Content.Client/Explosion/ExplosionSystem.cs @@ -2,7 +2,4 @@ namespace Content.Client.Explosion.EntitySystems; -public sealed class ExplosionSystem : SharedExplosionSystem -{ - -} +public sealed class ExplosionSystem : SharedExplosionSystem; diff --git a/Content.Client/Flash/FlashOverlay.cs b/Content.Client/Flash/FlashOverlay.cs index 046be2aa62142c..8e41c382dd7e7c 100644 --- a/Content.Client/Flash/FlashOverlay.cs +++ b/Content.Client/Flash/FlashOverlay.cs @@ -16,6 +16,7 @@ public sealed class FlashOverlay : Overlay [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IGameTiming _timing = default!; + private readonly SharedFlashSystem _flash; private readonly StatusEffectsSystem _statusSys; public override OverlaySpace Space => OverlaySpace.WorldSpace; @@ -27,6 +28,7 @@ public FlashOverlay() { IoCManager.InjectDependencies(this); _shader = _prototypeManager.Index("FlashedEffect").InstanceUnique(); + _flash = _entityManager.System(); _statusSys = _entityManager.System(); } @@ -41,7 +43,7 @@ protected override void FrameUpdate(FrameEventArgs args) || !_entityManager.TryGetComponent(playerEntity, out var status)) return; - if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time, status)) + if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status)) return; var curTime = _timing.CurTime; diff --git a/Content.Client/Mapping/MappingScreen.xaml b/Content.Client/Mapping/MappingScreen.xaml index b64136084788ac..9cc3e734f0e9ee 100644 --- a/Content.Client/Mapping/MappingScreen.xaml +++ b/Content.Client/Mapping/MappingScreen.xaml @@ -78,6 +78,7 @@ ToolTip="Pick (Hold 5)" /> + diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs index b2ad2fd83fbb8d..46c0e51fad69b4 100644 --- a/Content.Client/Mapping/MappingScreen.xaml.cs +++ b/Content.Client/Mapping/MappingScreen.xaml.cs @@ -96,6 +96,22 @@ public MappingScreen() Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png"; Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png"; + Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png"; + Flip.OnPressed += args => FlipSides(); + } + + public void FlipSides() + { + ScreenContainer.Flip(); + + if (SpawnContainer.GetPositionInParent() == 0) + { + Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png"; + } + else + { + Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png"; + } } private void OnDecalColorPicked(Color color) diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml index 660f2e5e11f5ed..dd40749d33b2bd 100644 --- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml +++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml @@ -15,6 +15,9 @@ + + departmentSens // Populate departments foreach (var sensor in departmentSensors) { + if (!string.IsNullOrEmpty(SearchLineEdit.Text) + && !sensor.Name.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase) + && !sensor.Job.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase)) + continue; + var coordinates = _entManager.GetCoordinates(sensor.Coordinates); // Add a button that will hold a username and other details diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs index 64ead32586d27e..2674343e059144 100644 --- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs +++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs @@ -199,7 +199,9 @@ protected override void Draw(DrawingHandleScreen handle) var gridMatrix = _transform.GetWorldMatrix(gUid); var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert); - var color = _shuttles.GetIFFColor(grid, self: false, iff); + + 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); // Others default: // Color.FromHex("#FFC000FF") @@ -213,25 +215,52 @@ protected override void Draw(DrawingHandleScreen handle) var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty); gridCentre.Y = -gridCentre.Y; + var distance = gridCentre.Length(); var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName), ("distance", $"{distance:0.0}")); + var mapCoords = _transform.GetWorldPosition(gUid); + var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})"; + // yes 1.0 scale is intended here. var labelDimensions = handle.GetDimensions(Font, labelText, 1f); + var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f); // 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 offset the matrix position to render it off by half its width - // plus by the offset. - var uiPosition = ScalePosition(gridCentre)- new Vector2(labelDimensions.X / 2f, -yOffset); + // 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); - // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport. - uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, PixelWidth - labelDimensions.X ), - Math.Clamp(uiPosition.Y, 0f, PixelHeight - labelDimensions.Y)); + // 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 + var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f); + var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f; + if (offsetMax > 1) + { + gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax); + + gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize; + } - handle.DrawString(Font, uiPosition, labelText, color); + var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0); + var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y); + + // clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport + // coord label intentionally isn't clamped so we don't get ugly clutter at the edges + var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y); + labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents); + + // draw IFF label + handle.DrawString(Font, labelUiPosition, labelText, labelColor); + + // only draw coords label if close enough + if (offsetMax < 1) + { + handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor); + } } // Detailed view @@ -241,7 +270,7 @@ protected override void Draw(DrawingHandleScreen handle) if (!gridAABB.Intersects(viewAABB)) continue; - DrawGrid(handle, matty, grid, color); + DrawGrid(handle, matty, grid, labelColor); DrawDocks(handle, gUid, matty); } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index b503999408f259..a5675af2aacafa 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -698,6 +698,18 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font-color", Color.FromHex("#E5E5E581")), }), + // ItemStatus for hands + Element() + .Class(StyleClassItemStatusNotHeld) + .Prop("font", notoSansItalic10) + .Prop("font-color", ItemStatusNotHeldColor) + .Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)), + + Element() + .Class(StyleClassItemStatus) + .Prop(nameof(RichTextLabel.LineHeightScale), 0.7f) + .Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)), + // Context Menu window Element().Class(ContextMenuPopup.StyleClassContextMenuPopup) .Prop(PanelContainer.StylePropertyPanel, contextMenuBackground), diff --git a/Content.Client/Wires/UI/WiresMenu.cs b/Content.Client/Wires/UI/WiresMenu.cs index eccc548297cd8c..77fc3accceb34a 100644 --- a/Content.Client/Wires/UI/WiresMenu.cs +++ b/Content.Client/Wires/UI/WiresMenu.cs @@ -584,17 +584,10 @@ public StatusLight(StatusLightData data, IResourceCache resourceCache) private sealed class HelpPopup : Popup { - private const string Text = "Click on the gold contacts with a multitool in hand to pulse their wire.\n" + - "Click on the wires with a pair of wirecutters in hand to cut/mend them.\n\n" + - "The lights at the top show the state of the machine, " + - "messing with wires will probably do stuff to them.\n" + - "Wire layouts are different each round, " + - "but consistent between machines of the same type."; - public HelpPopup() { var label = new RichTextLabel(); - label.SetMessage(Text); + label.SetMessage(Loc.GetString("wires-menu-help-popup")); AddChild(new PanelContainer { StyleClasses = {ExamineSystem.StyleClassEntityTooltip}, diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs new file mode 100644 index 00000000000000..d9cce764ab7dba --- /dev/null +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs @@ -0,0 +1,108 @@ +using Content.Shared.Buckle; +using Content.Shared.Buckle.Components; +using Content.Shared.Interaction; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Buckle; + +public sealed partial class BuckleTest +{ + [Test] + public async Task BuckleInteractUnbuckleOther() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var entMan = server.ResolveDependency(); + var buckleSystem = entMan.System(); + + EntityUid user = default; + EntityUid victim = default; + EntityUid chair = default; + BuckleComponent buckle = null; + StrapComponent strap = null; + + await server.WaitAssertion(() => + { + user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace); + victim = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace); + chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace); + + Assert.That(entMan.TryGetComponent(victim, out buckle)); + Assert.That(entMan.TryGetComponent(chair, out strap)); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Buckle victim to chair + Assert.That(buckleSystem.TryBuckle(victim, user, chair, buckle)); + Assert.Multiple(() => + { + Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair."); + Assert.That(buckle.Buckled, "Victim is not buckled."); + Assert.That(strap.BuckledEntities, Does.Contain(victim), "Chair does not have victim buckled to it."); + }); + + // InteractHand with chair to unbuckle victim + entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair)); + Assert.Multiple(() => + { + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(buckle.Buckled, Is.False); + Assert.That(strap.BuckledEntities, Does.Not.Contain(victim)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task BuckleInteractBuckleUnbuckleSelf() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var entMan = server.ResolveDependency(); + + EntityUid user = default; + EntityUid chair = default; + BuckleComponent buckle = null; + StrapComponent strap = null; + + await server.WaitAssertion(() => + { + user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace); + chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace); + + Assert.That(entMan.TryGetComponent(user, out buckle)); + Assert.That(entMan.TryGetComponent(chair, out strap)); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Buckle user to chair + entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair)); + Assert.Multiple(() => + { + Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair."); + Assert.That(buckle.Buckled, "Victim is not buckled."); + Assert.That(strap.BuckledEntities, Does.Contain(user), "Chair does not have victim buckled to it."); + }); + + // InteractHand with chair to unbuckle + entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair)); + Assert.Multiple(() => + { + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(buckle.Buckled, Is.False); + Assert.That(strap.BuckledEntities, Does.Not.Contain(user)); + }); + }); + + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index 156f42aac333c4..1b31fe38c2899d 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Buckle [TestFixture] [TestOf(typeof(BuckleComponent))] [TestOf(typeof(StrapComponent))] - public sealed class BuckleTest + public sealed partial class BuckleTest { private const string BuckleDummyId = "BuckleDummy"; private const string StrapDummyId = "StrapDummy"; diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs index ff4d34705a6c08..9240e7b91b666f 100644 --- a/Content.Server/Administration/Commands/SetOutfitCommand.cs +++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs @@ -4,11 +4,15 @@ using Content.Server.Preferences.Managers; using Content.Shared.Access.Components; using Content.Shared.Administration; +using Content.Shared.Clothing; using Content.Shared.Hands.Components; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Preferences; +using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; +using Content.Shared.Station; using Robust.Shared.Console; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -82,9 +86,11 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit return false; HumanoidCharacterProfile? profile = null; + ICommonSession? session = null; // Check if we are setting the outfit of a player to respect the preferences if (entityManager.TryGetComponent(target, out ActorComponent? actorComponent)) { + session = actorComponent.PlayerSession; var userId = actorComponent.PlayerSession.UserId; var preferencesManager = IoCManager.Resolve(); var prefs = preferencesManager.GetPreferences(userId); @@ -128,6 +134,36 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit } } + // See if this starting gear is associated with a job + var jobs = prototypeManager.EnumeratePrototypes(); + foreach (var job in jobs) + { + if (job.StartingGear != gear) + continue; + + var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID); + if (!prototypeManager.TryIndex(jobProtoId, out var jobProto)) + break; + + // Don't require a player, so this works on Urists + profile ??= entityManager.TryGetComponent(target, out var comp) + ? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species) + : new HumanoidCharacterProfile(); + // Try to get the user's existing loadout for the role + profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout); + + if (roleLoadout == null) + { + // If they don't have a loadout for the role, make a default one + roleLoadout = new RoleLoadout(jobProtoId); + roleLoadout.SetDefault(profile, session, prototypeManager); + } + + // Equip the target with the job loadout + var stationSpawning = entityManager.System(); + stationSpawning.EquipRoleLoadout(target, roleLoadout, jobProto); + } + return true; } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs index db952237338f79..a03f27b561ae90 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs @@ -1,19 +1,21 @@ using Content.Server.Atmos.Components; -using Content.Server.Atmos.Reactions; +using Content.Server.Decals; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Reactions; -using Content.Shared.Audio; using Content.Shared.Database; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Player; +using Robust.Shared.Random; namespace Content.Server.Atmos.EntitySystems { public sealed partial class AtmosphereSystem { + [Dependency] private readonly DecalSystem _decalSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + private const int HotspotSoundCooldownCycles = 200; private int _hotspotSoundCooldown = 0; @@ -56,7 +58,30 @@ private void ProcessHotspot( if (tile.Hotspot.Bypassing) { tile.Hotspot.State = 3; - // TODO ATMOS: Burn tile here + + var gridUid = ent.Owner; + var tilePos = tile.GridIndices; + + // Get the existing decals on the tile + var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos); + + // Count the burnt decals on the tile + var tileBurntDecals = 0; + + foreach (var set in tileDecals) + { + if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1) + continue; + + tileBurntDecals++; + + if (tileBurntDecals > 4) + break; + } + + // Add a random burned decal to the tile only if there are less than 4 of them + if (tileBurntDecals < 4) + _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true); if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread) { diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs index 44bfa4cc10c751..13d8f73dc56bd8 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Fluids.EntitySystems; using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Decals; using Content.Shared.Doors.Components; using Content.Shared.Maps; using JetBrains.Annotations; @@ -12,7 +13,9 @@ using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Random; +using System.Linq; namespace Content.Server.Atmos.EntitySystems; @@ -36,6 +39,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly TileSystem _tile = default!; [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] public readonly PuddleSystem Puddle = default!; private const float ExposedUpdateDelay = 1f; @@ -47,6 +51,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem private EntityQuery _firelockQuery; private HashSet _entSet = new(); + private string[] _burntDecals = []; + public override void Initialize() { base.Initialize(); @@ -66,7 +72,9 @@ public override void Initialize() _firelockQuery = GetEntityQuery(); SubscribeLocalEvent(OnTileChanged); + SubscribeLocalEvent(OnPrototypesReloaded); + CacheDecals(); } public override void Shutdown() @@ -81,6 +89,12 @@ private void OnTileChanged(ref TileChangedEvent ev) InvalidateTile(ev.NewTile.GridUid, ev.NewTile.GridIndices); } + private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev) + { + if (ev.WasModified()) + CacheDecals(); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -107,4 +121,9 @@ public override void Update(float frameTime) _exposedTimer -= ExposedUpdateDelay; } + + private void CacheDecals() + { + _burntDecals = _prototypeManager.EnumeratePrototypes().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray(); + } } diff --git a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs index cd422328c3e183..7caec6150ede0f 100644 --- a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs +++ b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs @@ -340,6 +340,9 @@ protected override void OnItemInserted(EntityUid uid, CartridgeLoaderComponent l if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID) return; + if (TryComp(args.Entity, out CartridgeComponent? cartridge)) + cartridge.LoaderUid = uid; + RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid)); base.OnItemInserted(uid, loader, args); } @@ -360,6 +363,9 @@ protected override void OnItemRemoved(EntityUid uid, CartridgeLoaderComponent lo if (deactivate) RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid)); + if (TryComp(args.Entity, out CartridgeComponent? cartridge)) + cartridge.LoaderUid = null; + RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid)); base.OnItemRemoved(uid, loader, args); diff --git a/Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs b/Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs new file mode 100644 index 00000000000000..08eef62379abae --- /dev/null +++ b/Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs @@ -0,0 +1,8 @@ +using Content.Shared.Security; + +namespace Content.Server.CartridgeLoader.Cartridges; + +[RegisterComponent] +public sealed partial class WantedListCartridgeComponent : Component +{ +} diff --git a/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs b/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs index 23bf6b215736e3..03c0ffe0c1ae9d 100644 --- a/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs +++ b/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs @@ -14,31 +14,31 @@ public sealed partial class SolutionRegenerationComponent : Component /// /// The name of the solution to add to. /// - [DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)] + [DataField("solution", required: true)] public string SolutionName = string.Empty; /// /// The solution to add reagents to. /// - [DataField("solutionRef")] - public Entity? Solution = null; + [DataField] + public Entity? SolutionRef = null; /// /// The reagent(s) to be regenerated in the solution. /// - [DataField("generated", required: true), ViewVariables(VVAccess.ReadWrite)] + [DataField(required: true)] public Solution Generated = default!; /// /// How long it takes to regenerate once. /// - [DataField("duration"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan Duration = TimeSpan.FromSeconds(1); /// /// The time when the next regeneration will occur. /// - [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0); } diff --git a/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs index 6a600628572dce..bccd5947069643 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs @@ -24,7 +24,7 @@ public override void Update(float frameTime) // timer ignores if its full, it's just a fixed cycle regen.NextRegenTime = _timing.CurTime + regen.Duration; - if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.Solution, out var solution)) + if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.SolutionRef, out var solution)) { var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume); if (amount <= FixedPoint2.Zero) @@ -41,7 +41,7 @@ public override void Update(float frameTime) generated = regen.Generated.Clone().SplitSolution(amount); } - _solutionContainer.TryAddSolution(regen.Solution.Value, generated); + _solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated); } } } diff --git a/Content.Server/Construction/Completions/GivePrototype.cs b/Content.Server/Construction/Completions/GivePrototype.cs index f45bd5ba105287..f05feb70c0346d 100644 --- a/Content.Server/Construction/Completions/GivePrototype.cs +++ b/Content.Server/Construction/Completions/GivePrototype.cs @@ -1,44 +1,50 @@ using Content.Server.Stack; using Content.Shared.Construction; +using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Prototypes; using Content.Shared.Stacks; using JetBrains.Annotations; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Construction.Completions +namespace Content.Server.Construction.Completions; + +[UsedImplicitly] +[DataDefinition] +public sealed partial class GivePrototype : IGraphAction { - [UsedImplicitly] - [DataDefinition] - public sealed partial class GivePrototype : IGraphAction + [DataField] + public EntProtoId Prototype { get; private set; } = string.Empty; + + [DataField] + public int Amount { get; private set; } = 1; + + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Prototype { get; private set; } = string.Empty; - [DataField("amount")] - public int Amount { get; private set; } = 1; + if (string.IsNullOrEmpty(Prototype)) + return; - public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) + if (EntityPrototypeHelpers.HasComponent(Prototype)) { - if (string.IsNullOrEmpty(Prototype)) - return; + var stackSystem = entityManager.EntitySysManager.GetEntitySystem(); + var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid); - var coordinates = entityManager.GetComponent(userUid ?? uid).Coordinates; + if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp)) + return; - if (EntityPrototypeHelpers.HasComponent(Prototype)) + foreach (var item in stacks) { - var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); - var stack = entityManager.GetComponent(stackEnt); - entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt, Amount, stack); - entityManager.EntitySysManager.GetEntitySystem().PickupOrDrop(userUid, stackEnt); + stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp); } - else + } + else + { + var handsSystem = entityManager.EntitySysManager.GetEntitySystem(); + var handsComp = userUid is not null ? entityManager.GetComponent(userUid.Value) : null; + for (var i = 0; i < Amount; i++) { - for (var i = 0; i < Amount; i++) - { - var item = entityManager.SpawnEntity(Prototype, coordinates); - entityManager.EntitySysManager.GetEntitySystem().PickupOrDrop(userUid, item); - } + var item = entityManager.SpawnNextToOrDrop(Prototype, userUid ?? uid); + handsSystem.PickupOrDrop(userUid, item, handsComp: handsComp); } } } diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index c5f1d159f31c43..ca1d45e6449466 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -68,6 +68,13 @@ private void OnFiltersChanged(Entity ent, ref S } } + private void GetOfficer(EntityUid uid, out string officer) + { + var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, uid); + RaiseLocalEvent(tryGetIdentityShortInfoEvent); + officer = tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("criminal-records-console-unknown-officer"); + } + private void OnChangeStatus(Entity ent, ref CriminalRecordChangeStatus msg) { // prevent malf client violating wanted/reason nullability @@ -90,29 +97,22 @@ private void OnChangeStatus(Entity ent, ref Cri return; } + var oldStatus = record.Status; + + var name = _records.RecordName(key.Value); + GetOfficer(mob.Value, out var officer); + // when arresting someone add it to history automatically // fallback exists if the player was not set to wanted beforehand if (msg.Status == SecurityStatus.Detained) { var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason"); var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason)); - _criminalRecords.TryAddHistory(key.Value, history); + _criminalRecords.TryAddHistory(key.Value, history, officer); } - var oldStatus = record.Status; - // will probably never fail given the checks above - _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason); - - var name = _records.RecordName(key.Value); - var officer = Loc.GetString("criminal-records-console-unknown-officer"); - - var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value); - RaiseLocalEvent(tryGetIdentityShortInfoEvent); - if (tryGetIdentityShortInfoEvent.Title != null) - { - officer = tryGetIdentityShortInfoEvent.Title; - } + _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer); (string, object)[] args; if (reason != null) @@ -152,14 +152,16 @@ private void OnChangeStatus(Entity ent, ref Cri private void OnAddHistory(Entity ent, ref CriminalRecordAddHistory msg) { - if (!CheckSelected(ent, msg.Actor, out _, out var key)) + if (!CheckSelected(ent, msg.Actor, out var mob, out var key)) return; var line = msg.Line.Trim(); if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength) return; - if (!_criminalRecords.TryAddHistory(key.Value, line)) + GetOfficer(mob.Value, out var officer); + + if (!_criminalRecords.TryAddHistory(key.Value, line, officer)) return; // no radio message since its not crucial to officers patrolling diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs index a65fb0be9e1a70..7c65ce8c248c58 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs @@ -1,10 +1,15 @@ -using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.CartridgeLoader; +using Content.Server.CartridgeLoader.Cartridges; using Content.Server.StationRecords.Systems; using Content.Shared.CriminalRecords; using Content.Shared.CriminalRecords.Systems; using Content.Shared.Security; using Content.Shared.StationRecords; using Content.Server.GameTicking; +using Content.Server.Station.Systems; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; namespace Content.Server.CriminalRecords.Systems; @@ -20,12 +25,18 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem { [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly StationRecordsSystem _records = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly CartridgeLoaderSystem _cartridge = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGeneralRecordCreated); + SubscribeLocalEvent(OnRecordChanged); + SubscribeLocalEvent(OnCartridgeUiReady); + SubscribeLocalEvent(OnHistoryAdded); + SubscribeLocalEvent(OnHistoryRemoved); } private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev) @@ -39,14 +50,14 @@ private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev) /// Reason should only be passed if status is Wanted, nullability isn't checked. /// /// True if the status is changed, false if not - public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason) + public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason, string? initiatorName = null) { // don't do anything if its the same status if (!_records.TryGetRecord(key, out var record) || status == record.Status) return false; - OverwriteStatus(key, record, status, reason); + OverwriteStatus(key, record, status, reason, initiatorName); return true; } @@ -54,16 +65,24 @@ public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? /// /// Sets the status without checking previous status or reason nullability. /// - public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason) + public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason, string? initiatorName = null) { record.Status = status; record.Reason = reason; + record.InitiatorName = initiatorName; var name = _records.RecordName(key); if (name != string.Empty) UpdateCriminalIdentity(name, status); _records.Synchronize(key); + + var args = new CriminalRecordChangedEvent(record); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var readerUid, out _)) + { + RaiseLocalEvent(readerUid, ref args); + } } /// @@ -76,15 +95,23 @@ public bool TryAddHistory(StationRecordKey key, CrimeHistory entry) return false; record.History.Add(entry); + + var args = new CriminalHistoryAddedEvent(entry); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var readerUid, out _)) + { + RaiseLocalEvent(readerUid, ref args); + } + return true; } /// /// Creates and tries to add a history entry using the current time. /// - public bool TryAddHistory(StationRecordKey key, string line) + public bool TryAddHistory(StationRecordKey key, string line, string? initiatorName = null) { - var entry = new CrimeHistory(_ticker.RoundDuration(), line); + var entry = new CrimeHistory(_ticker.RoundDuration(), line, initiatorName); return TryAddHistory(key, entry); } @@ -100,7 +127,58 @@ public bool TryDeleteHistory(StationRecordKey key, uint index) if (index >= record.History.Count) return false; + var history = record.History[(int)index]; record.History.RemoveAt((int) index); + + var args = new CriminalHistoryRemovedEvent(history); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var readerUid, out _)) + { + RaiseLocalEvent(readerUid, ref args); + } + return true; } + + private void OnRecordChanged(Entity ent, ref CriminalRecordChangedEvent args) => + StateChanged(ent); + + private void OnHistoryAdded(Entity ent, ref CriminalHistoryAddedEvent args) => + StateChanged(ent); + + private void OnHistoryRemoved(Entity ent, ref CriminalHistoryRemovedEvent args) => + StateChanged(ent); + + private void StateChanged(Entity ent) + { + if (Comp(ent).LoaderUid is not { } loaderUid) + return; + + UpdateReaderUi(ent, loaderUid); + } + + private void OnCartridgeUiReady(Entity ent, ref CartridgeUiReadyEvent args) + { + UpdateReaderUi(ent, args.Loader); + } + + private void UpdateReaderUi(Entity ent, EntityUid loaderUid) + { + if (_station.GetOwningStation(ent) is not { } station) + return; + + var records = _records.GetRecordsOfType(station) + .Where(cr => cr.Item2.Status is not SecurityStatus.None || cr.Item2.History.Count > 0) + .Select(cr => + { + var (i, r) = cr; + var key = new StationRecordKey(i, station); + // Hopefully it will work smoothly..... + _records.TryGetRecord(key, out GeneralStationRecord? generalRecord); + return new WantedRecord(generalRecord!, r.Status, r.Reason, r.InitiatorName, r.History); + }); + var state = new WantedListUiState(records.ToList()); + + _cartridge.UpdateCartridgeUiState(loaderUid, state); + } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs index 5166aaccabba41..08c7c8f068f81e 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs @@ -1,4 +1,4 @@ -using Content.Server.Explosion.Components; +using Content.Shared.Explosion.Components; using JetBrains.Annotations; namespace Content.Server.Destructible.Thresholds.Behaviors diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs index 4b59c8f1c48f8f..6fa553bc8b60d6 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs @@ -6,9 +6,10 @@ using Content.Shared.Explosion.EntitySystems; using Content.Shared.FixedPoint; using Robust.Shared.Map.Components; + namespace Content.Server.Explosion.EntitySystems; -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { [Dependency] private readonly DestructibleSystem _destructibleSystem = default!; diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.CVars.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.CVars.cs index ce98f89de7af4a..5af06ef93684af 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.CVars.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.CVars.cs @@ -1,8 +1,8 @@ using Content.Shared.CCVar; -using Content.Shared.Explosion.EntitySystems; + namespace Content.Server.Explosion.EntitySystems; -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { public int MaxIterations { get; private set; } public int MaxArea { get; private set; } diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs index 75bb606441a424..29477c16b28e33 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs @@ -12,7 +12,7 @@ namespace Content.Server.Explosion.EntitySystems; // A good portion of it is focused around keeping track of what tile-indices on a grid correspond to tiles that border // space. AFAIK no other system currently needs to track these "edge-tiles". If they do, this should probably be a // property of the grid itself? -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { /// /// Set of tiles of each grid that are directly adjacent to space, along with the directions that face space. diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs index 6d0cbcf2794c27..97d52e436a8f7e 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs @@ -22,9 +22,10 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; + namespace Content.Server.Explosion.EntitySystems; -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { [Dependency] private readonly FlammableSystem _flammableSystem = default!; @@ -218,7 +219,7 @@ internal bool ExplodeTile(BroadphaseComponent lookup, // get the entities on a tile. Note that we cannot process them directly, or we get // enumerator-changed-while-enumerating errors. List<(EntityUid, TransformComponent)> list = new(); - var state = (list, processed, _transformQuery); + var state = (list, processed, EntityManager.TransformQuery); // get entities: lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true); @@ -317,7 +318,7 @@ internal void ExplodeSpace(BroadphaseComponent lookup, var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize)); var worldBox = spaceMatrix.TransformBox(gridBox); var list = new List<(EntityUid, TransformComponent)>(); - var state = (list, processed, invSpaceMatrix, lookup.Owner, _transformQuery, gridBox, _transformSystem); + var state = (list, processed, invSpaceMatrix, lookup.Owner, EntityManager.TransformQuery, gridBox, _transformSystem); // get entities: lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true); diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs index 8c3229e06eef53..7b73490d9467f5 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs @@ -7,13 +7,13 @@ using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; using Robust.Shared.Timing; -using Content.Shared.Explosion.EntitySystems; + namespace Content.Server.Explosion.EntitySystems; // This partial part of the explosion system has all of the functions used to create the actual explosion map. // I.e, to get the sets of tiles & intensity values that describe an explosion. -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { /// /// This is the main explosion generating function. diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Visuals.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Visuals.cs index 219dba4bdeb8f8..57323e4de7093e 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Visuals.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Visuals.cs @@ -5,10 +5,11 @@ using Robust.Server.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; + namespace Content.Server.Explosion.EntitySystems; // This part of the system handled send visual / overlay data to clients. -public sealed partial class ExplosionSystem : SharedExplosionSystem +public sealed partial class ExplosionSystem { public void InitVisuals() { diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs index cd0ca1c22eb2fa..818953ed4b478b 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs @@ -12,6 +12,8 @@ using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Explosion; +using Content.Shared.Explosion.Components; +using Content.Shared.Explosion.EntitySystems; using Content.Shared.GameTicking; using Content.Shared.Inventory; using Content.Shared.Projectiles; @@ -53,7 +55,6 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedMapSystem _map = default!; - private EntityQuery _transformQuery; private EntityQuery _flammableQuery; private EntityQuery _physicsQuery; private EntityQuery _projectileQuery; @@ -103,7 +104,6 @@ public override void Initialize() InitAirtightMap(); InitVisuals(); - _transformQuery = GetEntityQuery(); _flammableQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _projectileQuery = GetEntityQuery(); @@ -141,15 +141,8 @@ private void OnGetResistance(EntityUid uid, ExplosionResistanceComponent compone args.DamageCoefficient *= modifier; } - /// - /// Given an entity with an explosive component, spawn the appropriate explosion. - /// - /// - /// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not - /// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a - /// solution in a reaction). - /// - public void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null) + /// + public override void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null) { // log missing: false, because some entities (e.g. liquid tanks) attempt to trigger explosions when damaged, // but may not actually be explosive. diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index ccb58e94f81426..fb449a372cd499 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -152,7 +152,7 @@ public void Flash(EntityUid target, } } - public void FlashArea(Entity source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) + public override void FlashArea(Entity source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) { var transform = Transform(source); var mapPosition = _transform.GetMapCoordinates(transform); diff --git a/Content.Server/Objectives/Systems/StealConditionSystem.cs b/Content.Server/Objectives/Systems/StealConditionSystem.cs index be34a80fe34850..e2d81e011cf5e2 100644 --- a/Content.Server/Objectives/Systems/StealConditionSystem.cs +++ b/Content.Server/Objectives/Systems/StealConditionSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Objectives.Components; using Content.Server.Objectives.Components.Targets; +using Content.Shared.CartridgeLoader; using Content.Shared.Mind; using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Systems; @@ -172,6 +173,11 @@ private int CheckStealTarget(EntityUid entity, StealConditionComponent condition if (target.StealGroup != condition.StealGroup) return 0; + // check if cartridge is installed + if (TryComp(entity, out var cartridge) && + cartridge.InstallationStatus is not InstallationStatus.Cartridge) + return 0; + // check if needed target alive if (condition.CheckAlive) { diff --git a/Content.Server/Power/Generation/Teg/TegSystem.cs b/Content.Server/Power/Generation/Teg/TegSystem.cs index 9fb7d5ff1f649b..027f2570402370 100644 --- a/Content.Server/Power/Generation/Teg/TegSystem.cs +++ b/Content.Server/Power/Generation/Teg/TegSystem.cs @@ -102,10 +102,6 @@ private void GeneratorExamined(EntityUid uid, TegGeneratorComponent component, E private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref AtmosDeviceUpdateEvent args) { - var tegGroup = GetNodeGroup(uid); - if (tegGroup is not { IsFullyBuilt: true }) - return; - var supplier = Comp(uid); var powerReceiver = Comp(uid); if (!powerReceiver.Powered) @@ -114,6 +110,10 @@ private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref return; } + var tegGroup = GetNodeGroup(uid); + if (tegGroup is not { IsFullyBuilt: true }) + return; + var circA = tegGroup.CirculatorA!.Owner; var circB = tegGroup.CirculatorB!.Owner; diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 465ff20d08c7ab..80af723bc67978 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -127,7 +127,7 @@ public bool IsRoundEndRequested() return _countdownTokenSource != null; } - public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station") + public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement") { var duration = DefaultCountdownDuration; @@ -145,7 +145,7 @@ public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = tr RequestRoundEnd(duration, requester, checkCooldown, text, name); } - public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station") + public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement") { if (_gameTicker.RunLevel != GameRunLevel.InRound) return; diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs index 05262f2999629b..6d97c8ccb3ddc7 100644 --- a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs @@ -37,6 +37,7 @@ public override void Initialize() SubscribeLocalEvent(OnUnanchorAttempt); SubscribeLocalEvent(OnComponentRemoved); SubscribeLocalEvent(PreventBreach); + SubscribeLocalEvent(OnMapInit); } public override void Update(float frameTime) @@ -61,6 +62,12 @@ public override void Update(float frameTime) #region Events + private void OnMapInit(Entity generator, ref MapInitEvent args) + { + if (generator.Comp.Enabled) + ChangeFieldVisualizer(generator); + } + /// /// A generator receives power from a source colliding with it. /// diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs index 7677e800fe9678..1a15e52a3c4311 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs @@ -1,5 +1,5 @@ +using System.Linq; using Content.Shared.Buckle.Components; -using Content.Shared.Cuffs.Components; using Content.Shared.DoAfter; using Content.Shared.DragDrop; using Content.Shared.IdentityManagement; @@ -84,15 +84,29 @@ private void OnStrapInteractHand(EntityUid uid, StrapComponent component, Intera if (!TryComp(args.User, out BuckleComponent? buckle)) return; - if (buckle.BuckledTo == null && component.BuckleOnInteractHand) + // Buckle self + if (buckle.BuckledTo == null && component.BuckleOnInteractHand && StrapHasSpace(uid, buckle, component)) + { TryBuckle(args.User, args.User, uid, buckle, popup: true); - else if (buckle.BuckledTo == uid) - TryUnbuckle(args.User, args.User, buckle, popup: true); - else + args.Handled = true; + return; + } + + // Unbuckle self + if (buckle.BuckledTo == uid && TryUnbuckle(args.User, args.User, buckle, popup: true)) + { + args.Handled = true; return; + } + + // Unbuckle others + if (component.BuckledEntities.TryFirstOrNull(out var buckled) && TryUnbuckle(buckled.Value, args.User)) + { + args.Handled = true; + return; + } // TODO BUCKLE add out bool for whether a pop-up was generated or not. - args.Handled = true; } private void OnBuckleInteractHand(Entity ent, ref InteractHandEvent args) diff --git a/Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs new file mode 100644 index 00000000000000..9d55e0c1636821 --- /dev/null +++ b/Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs @@ -0,0 +1,11 @@ +using Content.Shared.CriminalRecords; +using Content.Shared.CriminalRecords.Systems; +using Robust.Shared.Serialization; + +namespace Content.Shared.CartridgeLoader.Cartridges; + +[Serializable, NetSerializable] +public sealed class WantedListUiState(List records) : BoundUserInterfaceState +{ + public List Records = records; +} diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index f41fa2b22d2c53..f25273f40394c1 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -359,39 +359,76 @@ public bool TryInsertFromHand(EntityUid uid, ItemSlot slot, EntityUid user, Hand /// Useful for predicted interactions /// /// False if failed to insert item - public bool TryInsertEmpty(Entity ent, EntityUid item, EntityUid? user, bool excludeUserAudio = false) + public bool TryInsertEmpty(Entity ent, + EntityUid item, + EntityUid? user, + bool excludeUserAudio = false) { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + TryComp(user, out HandsComponent? handsComp); + + if (!TryGetAvailableSlot(ent, + item, + user == null ? null : (user.Value, handsComp), + out var itemSlot, + emptyOnly: true)) + return false; + + if (user != null && !_handsSystem.TryDrop(user.Value, item, handsComp: handsComp)) + return false; + + Insert(ent, itemSlot, item, user, excludeUserAudio: excludeUserAudio); + return true; + } + + /// + /// Tries to get any slot that the can be inserted into. + /// + /// Entity that is being inserted into. + /// Entity being inserted into . + /// Entity inserting into . + /// The ItemSlot on to insert into. + /// True only returns slots that are empty. + /// False returns any slot that is able to receive . + /// True when a slot is found. Otherwise, false. + public bool TryGetAvailableSlot(Entity ent, + EntityUid item, + Entity? userEnt, + [NotNullWhen(true)] out ItemSlot? itemSlot, + bool emptyOnly = false) + { + itemSlot = null; + + if (userEnt is { } user + && Resolve(user, ref user.Comp) + && _handsSystem.IsHolding(user, item)) + { + if (!_handsSystem.CanDrop(user, item, user.Comp)) + return false; + } + if (!Resolve(ent, ref ent.Comp, false)) return false; var slots = new List(); foreach (var slot in ent.Comp.Slots.Values) { - if (slot.ContainerSlot?.ContainedEntity != null) + if (emptyOnly && slot.ContainerSlot?.ContainedEntity != null) continue; - if (CanInsert(ent, item, user, slot)) + if (CanInsert(ent, item, userEnt, slot)) slots.Add(slot); } if (slots.Count == 0) return false; - if (user != null && _handsSystem.IsHolding(user.Value, item)) - { - if (!_handsSystem.TryDrop(user.Value, item)) - return false; - } - slots.Sort(SortEmpty); - foreach (var slot in slots) - { - if (TryInsert(ent, slot, item, user, excludeUserAudio: excludeUserAudio)) - return true; - } - - return false; + itemSlot = slots[0]; + return true; } private static int SortEmpty(ItemSlot a, ItemSlot b) diff --git a/Content.Shared/CriminalRecords/CriminalRecord.cs b/Content.Shared/CriminalRecords/CriminalRecord.cs index 0fe23d4395419b..5a023a9188c63b 100644 --- a/Content.Shared/CriminalRecords/CriminalRecord.cs +++ b/Content.Shared/CriminalRecords/CriminalRecord.cs @@ -23,6 +23,12 @@ public sealed record CriminalRecord [DataField] public string? Reason; + /// + /// The name of the person who changed the status. + /// + [DataField] + public string? InitiatorName; + /// /// Criminal history of the person. /// This should have charges and time served added after someone is detained. @@ -35,4 +41,4 @@ public sealed record CriminalRecord /// A line of criminal activity and the time it was added at. /// [Serializable, NetSerializable] -public record struct CrimeHistory(TimeSpan AddTime, string Crime); +public record struct CrimeHistory(TimeSpan AddTime, string Crime, string? InitiatorName); diff --git a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs index 96b33ab91bd308..d665d32f1ed21d 100644 --- a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs +++ b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs @@ -2,6 +2,8 @@ using Content.Shared.IdentityManagement.Components; using Content.Shared.Security; using Content.Shared.Security.Components; +using Content.Shared.StationRecords; +using Robust.Shared.Serialization; namespace Content.Shared.CriminalRecords.Systems; @@ -50,3 +52,22 @@ public void SetCriminalIcon(string name, SecurityStatus status, EntityUid charac Dirty(characterUid, record); } } + +[Serializable, NetSerializable] +public struct WantedRecord(GeneralStationRecord targetInfo, SecurityStatus status, string? reason, string? initiator, List history) +{ + public GeneralStationRecord TargetInfo = targetInfo; + public SecurityStatus Status = status; + public string? Reason = reason; + public string? Initiator = initiator; + public List History = history; +}; + +[ByRefEvent] +public record struct CriminalRecordChangedEvent(CriminalRecord Record); + +[ByRefEvent] +public record struct CriminalHistoryAddedEvent(CrimeHistory History); + +[ByRefEvent] +public record struct CriminalHistoryRemovedEvent(CrimeHistory History); diff --git a/Content.Server/Explosion/Components/ExplosiveComponent.cs b/Content.Shared/Explosion/Components/ExplosiveComponent.cs similarity index 76% rename from Content.Server/Explosion/Components/ExplosiveComponent.cs rename to Content.Shared/Explosion/Components/ExplosiveComponent.cs index 2b27a89d9db78f..bab7f5a7d6785c 100644 --- a/Content.Server/Explosion/Components/ExplosiveComponent.cs +++ b/Content.Shared/Explosion/Components/ExplosiveComponent.cs @@ -1,8 +1,7 @@ -using Content.Server.Explosion.EntitySystems; -using Content.Shared.Explosion; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.Explosion.EntitySystems; +using Robust.Shared.Prototypes; -namespace Content.Server.Explosion.Components; +namespace Content.Shared.Explosion.Components; /// /// Specifies an explosion that can be spawned by this entity. The explosion itself is spawned via -[RegisterComponent] +[RegisterComponent, Access(typeof(SharedExplosionSystem))] public sealed partial class ExplosiveComponent : Component { - /// /// The explosion prototype. This determines the damage types, the tile-break chance, and some visual /// information (e.g., the light that the explosion gives off). /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ExplosionType = default!; + [DataField(required: true)] + public ProtoId ExplosionType = default!; /// /// The maximum intensity the explosion can have on a single tile. This limits the maximum damage and tile /// break chance the explosion can achieve at any given location. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("maxIntensity")] + [DataField] public float MaxIntensity = 4; /// /// How quickly the intensity drops off as you move away from the epicenter. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("intensitySlope")] + [DataField] public float IntensitySlope = 1; /// @@ -47,38 +42,34 @@ public sealed partial class ExplosiveComponent : Component /// This number can be overridden by passing optional argument to . /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("totalIntensity")] + [DataField] public float TotalIntensity = 10; /// /// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger /// explosives that don't space tiles, without having to create a new explosion-type prototype. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("tileBreakScale")] + [DataField] public float TileBreakScale = 1f; /// /// Maximum number of times that an explosive can break a tile. Currently, for normal space stations breaking a /// tile twice will generally result in a vacuum. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("maxTileBreak")] + [DataField] public int MaxTileBreak = int.MaxValue; /// /// Whether this explosive should be able to create a vacuum by breaking tiles. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("canCreateVacuum")] + [DataField] public bool CanCreateVacuum = true; /// /// An override for whether or not the entity should be deleted after it explodes. /// If null, the system calling the explode method handles it. /// - [DataField("deleteAfterExplosion")] + [DataField] public bool? DeleteAfterExplosion; /// diff --git a/Content.Shared/Explosion/EntitySystems/SharedExplosionSystem.cs b/Content.Shared/Explosion/EntitySystems/SharedExplosionSystem.cs index 1d926dd0b67d6b..f29825580784b6 100644 --- a/Content.Shared/Explosion/EntitySystems/SharedExplosionSystem.cs +++ b/Content.Shared/Explosion/EntitySystems/SharedExplosionSystem.cs @@ -1,25 +1,40 @@ -using Content.Shared.Explosion.Components; using Content.Shared.Armor; +using Content.Shared.Explosion.Components; namespace Content.Shared.Explosion.EntitySystems; +/// +/// Lets code in shared trigger explosions and handles explosion resistance examining. +/// All processing is still done clientside. +/// public abstract class SharedExplosionSystem : EntitySystem { - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnArmorExamine); } - private void OnArmorExamine(EntityUid uid, ExplosionResistanceComponent component, ref ArmorExamineEvent args) + private void OnArmorExamine(Entity ent, ref ArmorExamineEvent args) { - var value = MathF.Round((1f - component.DamageCoefficient) * 100, 1); + var value = MathF.Round((1f - ent.Comp.DamageCoefficient) * 100, 1); if (value == 0) return; args.Msg.PushNewline(); - args.Msg.AddMarkupOrThrow(Loc.GetString(component.Examine, ("value", value))); + args.Msg.AddMarkupOrThrow(Loc.GetString(ent.Comp.Examine, ("value", value))); + } + + /// + /// Given an entity with an explosive component, spawn the appropriate explosion. + /// + /// + /// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not + /// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a + /// solution in a reaction). + /// + public virtual void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null) + { } } diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs index f83f02a310586b..b7788098870b1f 100644 --- a/Content.Shared/Flash/SharedFlashSystem.cs +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -1,10 +1,15 @@ +using Content.Shared.Flash.Components; using Content.Shared.StatusEffect; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; -namespace Content.Shared.Flash +namespace Content.Shared.Flash; + +public abstract class SharedFlashSystem : EntitySystem { - public abstract class SharedFlashSystem : EntitySystem + public ProtoId FlashedKey = "Flashed"; + + public virtual void FlashArea(Entity source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) { - [ValidatePrototypeId] - public const string FlashedKey = "Flashed"; } } diff --git a/Content.Shared/Fluids/Components/DrainComponent.cs b/Content.Shared/Fluids/Components/DrainComponent.cs index 4fb4fe9438335c..50cb5f51958192 100644 --- a/Content.Shared/Fluids/Components/DrainComponent.cs +++ b/Content.Shared/Fluids/Components/DrainComponent.cs @@ -23,14 +23,14 @@ public sealed partial class DrainComponent : Component [DataField] public Entity? Solution = null; - [DataField("accumulator")] + [DataField] public float Accumulator = 0f; /// /// Does this drain automatically absorb surrouding puddles? Or is it a drain designed to empty - /// solutions in it manually? + /// solutions in it manually? /// - [DataField("autoDrain"), ViewVariables(VVAccess.ReadOnly)] + [DataField] public bool AutoDrain = true; /// @@ -38,47 +38,47 @@ public sealed partial class DrainComponent : Component /// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle. /// This will stay fixed to 1 second no matter what DrainFrequency is. /// - [DataField("unitsPerSecond")] + [DataField] public float UnitsPerSecond = 6f; /// /// How many units are ejected from the buffer per second. /// - [DataField("unitsDestroyedPerSecond")] + [DataField] public float UnitsDestroyedPerSecond = 3f; /// /// How many (unobstructed) tiles away the drain will /// drain puddles from. /// - [DataField("range"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float Range = 2f; /// /// How often in seconds the drain checks for puddles around it. /// If the EntityQuery seems a bit unperformant this can be increased. /// - [DataField("drainFrequency")] + [DataField] public float DrainFrequency = 1f; /// /// How much time it takes to unclog it with a plunger /// - [DataField("unclogDuration"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float UnclogDuration = 1f; /// /// What's the probability of uncloging on each try /// - [DataField("unclogProbability"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float UnclogProbability = 0.75f; - [DataField("manualDrainSound"), ViewVariables(VVAccess.ReadOnly)] + [DataField] public SoundSpecifier ManualDrainSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg"); - [DataField("plungerSound"), ViewVariables(VVAccess.ReadOnly)] + [DataField] public SoundSpecifier PlungerSound = new SoundPathSpecifier("/Audio/Items/Janitor/plunger.ogg"); - [DataField("unclogSound"), ViewVariables(VVAccess.ReadOnly)] + [DataField] public SoundSpecifier UnclogSound = new SoundPathSpecifier("/Audio/Effects/Fluids/glug.ogg"); } diff --git a/Content.Shared/Interaction/Events/InteractionFailureEvent.cs b/Content.Shared/Interaction/Events/InteractionFailureEvent.cs new file mode 100644 index 00000000000000..a820048104d0f5 --- /dev/null +++ b/Content.Shared/Interaction/Events/InteractionFailureEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Interaction.Events; + +/// +/// Raised on the target when failing to pet/hug something. +/// +[ByRefEvent] +public readonly record struct InteractionFailureEvent(EntityUid User); diff --git a/Content.Shared/Interaction/Events/InteractionSuccessEvent.cs b/Content.Shared/Interaction/Events/InteractionSuccessEvent.cs new file mode 100644 index 00000000000000..da4f9e43d7d263 --- /dev/null +++ b/Content.Shared/Interaction/Events/InteractionSuccessEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Interaction.Events; + +/// +/// Raised on the target when successfully petting/hugging something. +/// +[ByRefEvent] +public readonly record struct InteractionSuccessEvent(EntityUid User); diff --git a/Content.Shared/Interaction/InteractionPopupSystem.cs b/Content.Shared/Interaction/InteractionPopupSystem.cs index 2a742d4211b6cf..20c079dfd8c8d0 100644 --- a/Content.Shared/Interaction/InteractionPopupSystem.cs +++ b/Content.Shared/Interaction/InteractionPopupSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Bed.Sleep; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; @@ -100,6 +101,9 @@ private void SharedInteract( if (component.InteractSuccessSpawn != null) Spawn(component.InteractSuccessSpawn, _transform.GetMapCoordinates(uid)); + + var ev = new InteractionSuccessEvent(user); + RaiseLocalEvent(target, ref ev); } else { @@ -111,6 +115,9 @@ private void SharedInteract( if (component.InteractFailureSpawn != null) Spawn(component.InteractFailureSpawn, _transform.GetMapCoordinates(uid)); + + var ev = new InteractionFailureEvent(user); + RaiseLocalEvent(target, ref ev); } if (!string.IsNullOrEmpty(component.MessagePerceivedByOthers)) diff --git a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs index a9a9390a6e0e98..c332064ea38eae 100644 --- a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs +++ b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; +using Content.Shared.Storage; using Content.Shared.Storage.Components; namespace Content.Shared.Placeable; @@ -8,12 +9,16 @@ namespace Content.Shared.Placeable; public sealed class PlaceableSurfaceSystem : EntitySystem { [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAfterInteractUsing); + SubscribeLocalEvent(OnStorageInteractUsingAttempt); + SubscribeLocalEvent(OnStorageAfterOpen); + SubscribeLocalEvent(OnStorageAfterClose); } public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null) @@ -21,6 +26,9 @@ public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceCompon if (!Resolve(uid, ref surface, false)) return; + if (surface.IsPlaceable == isPlaceable) + return; + surface.IsPlaceable = isPlaceable; Dirty(uid, surface); } @@ -59,11 +67,24 @@ private void OnAfterInteractUsing(EntityUid uid, PlaceableSurfaceComponent surfa if (!_handsSystem.TryDrop(args.User, args.Used)) return; - if (surface.PlaceCentered) - Transform(args.Used).LocalPosition = Transform(uid).LocalPosition + surface.PositionOffset; - else - Transform(args.Used).Coordinates = args.ClickLocation; + _transformSystem.SetCoordinates(args.Used, + surface.PlaceCentered ? Transform(uid).Coordinates.Offset(surface.PositionOffset) : args.ClickLocation); args.Handled = true; } + + private void OnStorageInteractUsingAttempt(Entity ent, ref StorageInteractUsingAttemptEvent args) + { + args.Cancelled = true; + } + + private void OnStorageAfterOpen(Entity ent, ref StorageAfterOpenEvent args) + { + SetPlaceable(ent.Owner, true, ent.Comp); + } + + private void OnStorageAfterClose(Entity ent, ref StorageAfterCloseEvent args) + { + SetPlaceable(ent.Owner, false, ent.Comp); + } } diff --git a/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs b/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs index 938b34f354a6e0..6b09edfa1f0068 100644 --- a/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs +++ b/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs @@ -79,7 +79,7 @@ public int PowerBuffer /// /// Is the generator toggled on? /// - [ViewVariables] + [DataField] public bool Enabled; /// diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index 000d3f3cc396d9..5789ac51efc828 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -14,6 +14,7 @@ public sealed class StatusEffectsSystem : EntitySystem [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; + private List _toRemove = new(); public override void Initialize() { @@ -32,18 +33,28 @@ public override void Update(float frameTime) var curTime = _gameTiming.CurTime; var enumerator = EntityQueryEnumerator(); + _toRemove.Clear(); while (enumerator.MoveNext(out var uid, out _, out var status)) { - foreach (var state in status.ActiveEffects.ToArray()) + if (status.ActiveEffects.Count == 0) + { + // This shouldn't happen, but just in case something sneaks through + _toRemove.Add(uid); + continue; + } + + foreach (var state in status.ActiveEffects) { - // if we're past the end point of the effect if (curTime > state.Value.Cooldown.Item2) - { TryRemoveStatusEffect(uid, state.Key, status); - } } } + + foreach (var uid in _toRemove) + { + RemComp(uid); + } } private void OnGetState(EntityUid uid, StatusEffectsComponent component, ref ComponentGetState args) @@ -62,29 +73,21 @@ private void OnHandleState(EntityUid uid, StatusEffectsComponent component, ref component.AllowedEffects.AddRange(state.AllowedEffects); // Remove non-existent effects. - foreach (var effect in component.ActiveEffects.Keys) + foreach (var key in component.ActiveEffects.Keys) { - if (!state.ActiveEffects.ContainsKey(effect)) - { - TryRemoveStatusEffect(uid, effect, component, remComp: false); - } + if (!state.ActiveEffects.ContainsKey(key)) + component.ActiveEffects.Remove(key); } foreach (var (key, effect) in state.ActiveEffects) { - // don't bother with anything if we already have it - if (component.ActiveEffects.ContainsKey(key)) - { - component.ActiveEffects[key] = new(effect); - continue; - } - - var time = effect.Cooldown.Item2 - effect.Cooldown.Item1; - - TryAddStatusEffect(uid, key, time, true, component, effect.Cooldown.Item1); - component.ActiveEffects[key].RelevantComponent = effect.RelevantComponent; - // state handling should not add networked components, that is handled separately by the client game state manager. + component.ActiveEffects[key] = new(effect); } + + if (component.ActiveEffects.Count == 0) + RemComp(uid); + else + EnsureComp(uid); } private void OnRejuvenate(EntityUid uid, StatusEffectsComponent component, RejuvenateEvent args) @@ -109,18 +112,16 @@ public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool if (!Resolve(uid, ref status, false)) return false; - if (TryAddStatusEffect(uid, key, time, refresh, status)) - { - // If they already have the comp, we just won't bother updating anything. - if (!EntityManager.HasComponent(uid)) - { - var comp = EntityManager.AddComponent(uid); - status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName(comp.GetType()); - } + if (!TryAddStatusEffect(uid, key, time, refresh, status)) + return false; + + if (HasComp(uid)) return true; - } - return false; + EntityManager.AddComponent(uid); + status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName(); + return true; + } public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, string component, @@ -162,8 +163,12 @@ public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool re /// If the effect already exists, it will simply replace the cooldown with the new one given. /// If you want special 'effect merging' behavior, do it your own damn self! /// - public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, - StatusEffectsComponent? status = null, TimeSpan? startTime = null) + public bool TryAddStatusEffect(EntityUid uid, + string key, + TimeSpan time, + bool refresh, + StatusEffectsComponent? status = null, + TimeSpan? startTime = null) { if (!Resolve(uid, ref status, false)) return false; @@ -334,8 +339,7 @@ public bool HasStatusEffect(EntityUid uid, string key, /// The entity to check on. /// The status effect ID to check for /// The status effect component, should you already have it. - public bool CanApplyEffect(EntityUid uid, string key, - StatusEffectsComponent? status = null) + public bool CanApplyEffect(EntityUid uid, string key, StatusEffectsComponent? status = null) { // don't log since stuff calling this prolly doesn't care if we don't actually have it if (!Resolve(uid, ref status, false)) diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index 4932613d0e640d..309ac0a2e09358 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -487,9 +487,6 @@ private void ModifyComponents(EntityUid uid, SharedEntityStorageComponent? compo } } - if (TryComp(uid, out var surface)) - _placeableSurface.SetPlaceable(uid, component.Open, surface); - _appearance.SetData(uid, StorageVisuals.Open, component.Open); _appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0); } diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index def9d797c4833c..d6fde292a14786 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -364,7 +364,9 @@ private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, Intera if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert, false)) return; - if (HasComp(uid)) + var attemptEv = new StorageInteractUsingAttemptEvent(); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) return; PlayerInsertHeldEntity((uid, storageComp), args.User); diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index a666169f529481..d2c607e57f7656 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -238,6 +238,9 @@ public AnimateInsertingEntitiesEvent(NetEntity storage, List storedEn [ByRefEvent] public record struct StorageInteractAttemptEvent(bool Silent, bool Cancelled = false); + [ByRefEvent] + public record struct StorageInteractUsingAttemptEvent(bool Cancelled = false); + [NetSerializable] [Serializable] public enum StorageVisuals : byte diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 11609a0efc96a3..5f5d96f41c9df4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,79 +1,4 @@ Entries: -- author: Plykiya - changes: - - message: You can now drop food and drinks to stop consuming it. - type: Fix - id: 6891 - time: '2024-07-09T23:12:40.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29854 -- author: Boaz1111 - changes: - - message: Guitars can now be worn in the suit storage slot - type: Add - id: 6892 - time: '2024-07-09T23:28:10.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29048 -- author: Winkarst-cpu - changes: - - message: Fixed popup spam when trying to open borg's UI while the borg is locked. - type: Fix - id: 6893 - time: '2024-07-09T23:48:56.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29861 -- author: Lokachop - changes: - - message: Scarves now count as warm clothing for the warm clothing cargo bounty. - type: Tweak - id: 6894 - time: '2024-07-10T05:26:33.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29779 -- author: Aquif - changes: - - message: It is now possible to "lock" admin faxes such that they cannot be edited - by cybersun pens or any other IC means. - type: Add - id: 6895 - time: '2024-07-10T05:28:36.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/28972 -- author: Ghagliiarghii - changes: - - message: The Librarian's Books Bag can now hold D&D related items such as dice - and battlemats. - type: Tweak - id: 6896 - time: '2024-07-10T05:51:01.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29863 -- author: Beck Thompson, Tayrtahn - changes: - - message: Typing indicators now correctly stack and will not overwrite your default - species indicator. - type: Fix - id: 6897 - time: '2024-07-10T05:51:48.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29492 -- author: Winkarst-cpu - changes: - - message: Now confirmation popup is displayed and item panel status is updated - after setting a custom solution transfer volume. - type: Fix - id: 6898 - time: '2024-07-10T10:32:30.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29852 -- author: Winkarst-cpu - changes: - - message: Added exit confirmation for character setup menu with unsaved changes. - type: Add - id: 6899 - time: '2024-07-11T00:24:37.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29875 -- author: ShadowCommander - changes: - - message: Players can now use melee attacks and shoves while dragging an entity - in their active hand. - type: Tweak - id: 6900 - time: '2024-07-11T04:48:00.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/29703 - author: Cojoke-dot changes: - message: You can no longer shoot out of crates with guns @@ -3917,3 +3842,82 @@ id: 7390 time: '2024-09-17T22:09:55.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/31951 +- author: Calecute + changes: + - message: Corrected cake batter recipe in guidebook + type: Fix + id: 7391 + time: '2024-09-18T15:15:34.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/32276 +- author: Beck Thompson + changes: + - message: Recycler no longer allows basic materials to be inserted into it. + type: Fix + id: 7392 + time: '2024-09-18T21:58:59.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/32144 +- author: deltanedas + changes: + - message: Epinephrine now adds Adrenaline, because it is. + type: Tweak + id: 7393 + time: '2024-09-18T23:00:48.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/32076 +- author: ShadowCommander + changes: + - message: Fixed clicking on chairs and beds with an entity buckled to them not + unbuckling them. + type: Fix + id: 7394 + time: '2024-09-18T23:55:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29998 +- author: Winkarst-cpu + changes: + - message: Now fire leaves burn marks on the tiles that were affected by it. + type: Add + id: 7395 + time: '2024-09-19T00:23:50.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/31939 +- author: ArchRBX + changes: + - message: Mass scanners and shuttle consoles now display coordinates beneath IFF + labels + type: Add + - message: IFF labels that are beyond the viewport extents maintain their heading + and don't hug corners + type: Fix + id: 7396 + time: '2024-09-19T01:25:47.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/31501 +- author: coffeeware + changes: + - message: a powered TEG won't produce infinite power when destroyed + type: Fix + id: 7397 + time: '2024-09-19T02:15:44.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29972 +- author: Boaz1111 + changes: + - message: Added plasma and uranium arrows. + type: Add + id: 7398 + time: '2024-09-19T08:41:24.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/31241 +- author: Ertanic + changes: + - message: Wanted list program and its cartridge. + type: Add + - message: The cartridge has been added to the HOS locker. + type: Add + - message: Added target to thief on wanted list cartridge. + type: Add + id: 7399 + time: '2024-09-19T10:22:02.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/31223 +- author: Errant + changes: + - message: Crew monitor list can now be filtered by name and job. + type: Add + id: 7400 + time: '2024-09-19T10:23:45.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/31659 diff --git a/Resources/Locale/en-US/actions/ui/actionslot.ftl b/Resources/Locale/en-US/actions/ui/actionslot.ftl index 332054f10e9d16..c0deaad248c2c0 100644 --- a/Resources/Locale/en-US/actions/ui/actionslot.ftl +++ b/Resources/Locale/en-US/actions/ui/actionslot.ftl @@ -1,2 +1,2 @@ ui-actionslot-charges = Uses left: {$charges} - +ui-actionslot-duration = [color=#a10505] {$duration} sec cooldown ({$timeLeft} sec remaining)[/color] diff --git a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl index f5cda2f2a18b21..2db27f5be09aec 100644 --- a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl @@ -19,3 +19,32 @@ log-probe-scan = Downloaded logs from {$device}! log-probe-label-time = Time log-probe-label-accessor = Accessed by log-probe-label-number = # + +# Wanted list cartridge +wanted-list-program-name = Wanted list +wanted-list-label-no-records = It's all right, cowboy +wanted-list-search-placeholder = Search by name and status + +wanted-list-age-label = [color=darkgray]Age:[/color] [color=white]{$age}[/color] +wanted-list-job-label = [color=darkgray]Job:[/color] [color=white]{$job}[/color] +wanted-list-species-label = [color=darkgray]Species:[/color] [color=white]{$species}[/color] +wanted-list-gender-label = [color=darkgray]Gender:[/color] [color=white]{$gender}[/color] + +wanted-list-reason-label = [color=darkgray]Reason:[/color] [color=white]{$reason}[/color] +wanted-list-unknown-reason-label = unknown reason + +wanted-list-initiator-label = [color=darkgray]Initiator:[/color] [color=white]{$initiator}[/color] +wanted-list-unknown-initiator-label = unknown initiator + +wanted-list-status-label = [color=darkgray]status:[/color] {$status -> + [suspected] [color=yellow]suspected[/color] + [wanted] [color=red]wanted[/color] + [detained] [color=#b18644]detained[/color] + [paroled] [color=green]paroled[/color] + [discharged] [color=green]discharged[/color] + *[other] none + } + +wanted-list-history-table-time-col = Time +wanted-list-history-table-reason-col = Crime +wanted-list-history-table-initiator-col = Initiator diff --git a/Resources/Locale/en-US/criminal-records/criminal-records.ftl b/Resources/Locale/en-US/criminal-records/criminal-records.ftl index 6d6a97300c2397..2a7c09912fae1a 100644 --- a/Resources/Locale/en-US/criminal-records/criminal-records.ftl +++ b/Resources/Locale/en-US/criminal-records/criminal-records.ftl @@ -39,7 +39,7 @@ criminal-records-console-released = {$name} has been released by {$officer}. criminal-records-console-not-wanted = {$officer} cleared the wanted status of {$name}. criminal-records-console-paroled = {$name} has been released on parole by {$officer}. criminal-records-console-not-parole = {$officer} cleared the parole status of {$name}. -criminal-records-console-unknown-officer = +criminal-records-console-unknown-officer = ## Filters diff --git a/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl index 7fd7f4608e16e9..601c45e4e22303 100644 --- a/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl +++ b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl @@ -2,6 +2,8 @@ crew-monitoring-user-interface-title = Crew Monitoring Console +crew-monitor-filter-line-placeholder = Filter + crew-monitoring-user-interface-name = Name crew-monitoring-user-interface-job = Job crew-monitoring-user-interface-status = Status diff --git a/Resources/Locale/en-US/objectives/conditions/steal-target-groups.ftl b/Resources/Locale/en-US/objectives/conditions/steal-target-groups.ftl index 48482bb2bb6111..b0a9d214ba3719 100644 --- a/Resources/Locale/en-US/objectives/conditions/steal-target-groups.ftl +++ b/Resources/Locale/en-US/objectives/conditions/steal-target-groups.ftl @@ -41,6 +41,7 @@ steal-target-groups-bible = bible steal-target-groups-clothing-neck-goldmedal = gold medal of crewmanship steal-target-groups-clothing-neck-clownmedal = clown medal steal-target-groups-recruiter-pen = recruiter pen +steal-target-groups-wanted-list-cartridge = wanted list cartridge # Thief structures steal-target-groups-teg = teg generator part diff --git a/Resources/Locale/en-US/round-end/round-end-system.ftl b/Resources/Locale/en-US/round-end/round-end-system.ftl index f86851506bc5ea..30069f71713903 100644 --- a/Resources/Locale/en-US/round-end/round-end-system.ftl +++ b/Resources/Locale/en-US/round-end/round-end-system.ftl @@ -4,6 +4,7 @@ round-end-system-shuttle-called-announcement = An emergency shuttle has been sen round-end-system-shuttle-already-called-announcement = An emergency shuttle has already been sent. round-end-system-shuttle-auto-called-announcement = An automatic crew shift change shuttle has been sent. ETA: {$time} {$units}. Recall the shuttle to extend the shift. round-end-system-shuttle-recalled-announcement = The emergency shuttle has been recalled. +round-end-system-shuttle-sender-announcement = Station round-end-system-round-restart-eta-announcement = Restarting the round in {$time} {$units}... eta-units-minutes = minutes diff --git a/Resources/Locale/en-US/wires/components/wires-component.ftl b/Resources/Locale/en-US/wires/components/wires-component.ftl index be27c270bb4d58..e98e5c21cab286 100644 --- a/Resources/Locale/en-US/wires/components/wires-component.ftl +++ b/Resources/Locale/en-US/wires/components/wires-component.ftl @@ -10,3 +10,9 @@ wires-component-ui-on-receive-message-cannot-mend-uncut-wire = You can't mend a wires-menu-name-label = Wires wires-menu-dead-beef-text = DEAD-BEEF +wires-menu-help-popup = + Click on the gold contacts with a multitool in hand to pulse their wire. + Click on the wires with a pair of wirecutters in hand to cut/mend them. + + The lights at the top show the state of the machine, messing with wires will probably do stuff to them. + Wire layouts are different each round, but consistent between machines of the same type. diff --git a/Resources/Maps/bagel.yml b/Resources/Maps/bagel.yml index b1712b7fa10a1e..508f08a2254c34 100644 --- a/Resources/Maps/bagel.yml +++ b/Resources/Maps/bagel.yml @@ -154176,12 +154176,36 @@ entities: parent: 60 - proto: WindoorSecure entities: + - uid: 2538 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 13.5,-11.5 + parent: 60 + - type: DeviceLinkSink + invokeCounter: 1 + - type: DeviceLinkSource + linkedPorts: + 3481: + - DoorStatus: DoorBolt - uid: 3269 components: - type: Transform rot: -1.5707963267948966 rad pos: 23.5,-46.5 parent: 60 + - uid: 3481 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 14.5,-13.5 + parent: 60 + - type: DeviceLinkSink + invokeCounter: 1 + - type: DeviceLinkSource + linkedPorts: + 2538: + - DoorStatus: DoorBolt - uid: 3911 components: - type: Transform @@ -154237,30 +154261,6 @@ entities: rot: -1.5707963267948966 rad pos: -23.5,-9.5 parent: 60 - - uid: 2538 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 14.5,-13.5 - parent: 60 - - type: DeviceLinkSink - invokeCounter: 1 - - type: DeviceLinkSource - linkedPorts: - 3481: - - DoorStatus: DoorBolt - - uid: 3481 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 13.5,-11.5 - parent: 60 - - type: DeviceLinkSink - invokeCounter: 1 - - type: DeviceLinkSource - linkedPorts: - 2538: - - DoorStatus: DoorBolt - uid: 12188 components: - type: Transform diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index d3189630160086..31ebad61837032 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -325,6 +325,7 @@ - id: RubberStampHos - id: SecurityTechFabCircuitboard - id: WeaponDisabler + - id: WantedListCartridge # Hardsuit table, used for suit storage as well - type: entityTable diff --git a/Resources/Prototypes/Decals/burnt.yml b/Resources/Prototypes/Decals/burnt.yml new file mode 100644 index 00000000000000..d9d500e1aa2529 --- /dev/null +++ b/Resources/Prototypes/Decals/burnt.yml @@ -0,0 +1,31 @@ +- type: decal + id: burnt1 + tags: ["burnt"] + defaultCleanable: true + sprite: + sprite: Decals/burnt.rsi + state: burnt1 + +- type: decal + id: burnt2 + tags: ["burnt"] + defaultCleanable: true + sprite: + sprite: Decals/burnt.rsi + state: burnt2 + +- type: decal + id: burnt3 + tags: ["burnt"] + defaultCleanable: true + sprite: + sprite: Decals/burnt.rsi + state: burnt3 + +- type: decal + id: burnt4 + tags: ["burnt"] + defaultCleanable: true + sprite: + sprite: Decals/burnt.rsi + state: burnt4 diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index 368cefe9cb4ed3..467bbf873f23d2 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -100,7 +100,7 @@ parent: ClothingEyesBase id: ClothingEyesGlassesJamjar name: jamjar glasses - description: Also known as Virginity Protectors. + description: These retro glasses remind you of a simpler time. components: - type: Sprite sprite: Clothing/Eyes/Glasses/jamjar.rsi diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 493cfdf6cb63e6..15878a4017d00c 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -462,7 +462,7 @@ - type: GhostRole name: ghost-role-information-syndicate-cyborg-assault-name description: ghost-role-information-syndicate-cyborg-description - rules: ghost-role-information-rules-default-silicon + rules: ghost-role-information-silicon-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -495,7 +495,7 @@ - type: GhostRole name: ghost-role-information-syndicate-cyborg-saboteur-name description: ghost-role-information-syndicate-cyborg-description - rules: ghost-role-information-rules-default-silicon + rules: ghost-role-information-silicon-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml index 1a7a02e7337220..71f98d81c96dbe 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml @@ -100,7 +100,7 @@ - type: GhostRole name: ghost-role-information-syndicate-cyborg-assault-name description: ghost-role-information-syndicate-cyborg-description - rules: ghost-role-information-rules-default-silicon + rules: ghost-role-information-silicon-rules raffle: settings: default - type: GhostRoleMobSpawner diff --git a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml index f9581149e2146b..91493f48cd1f50 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml @@ -93,3 +93,26 @@ - type: GuideHelp guides: - Forensics + +- type: entity + parent: BaseItem + id: WantedListCartridge + name: Wanted list cartridge + description: A program to get a list of wanted persons. + components: + - type: Sprite + sprite: Objects/Devices/cartridge.rsi + state: cart-sec + - type: Icon + sprite: Objects/Devices/cartridge.rsi + state: cart-sec + - type: UIFragment + ui: !type:WantedListUi + - type: Cartridge + programName: wanted-list-program-name + icon: + sprite: Objects/Misc/books.rsi + state: icon_magnifier + - type: WantedListCartridge + - type: StealTarget + stealGroup: WantedListCartridge diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 2d0171fb034131..57ec79f598af8f 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -114,6 +114,18 @@ - type: Speech speechVerb: Robotic +- type: entity + id: BaseSecurityPDA + abstract: true + components: + - type: CartridgeLoader + uiKey: enum.PdaUiKey.Key + preinstalled: + - CrewManifestCartridge + - NotekeeperCartridge + - NewsReaderCartridge + - WantedListCartridge + - type: entity parent: BasePDA id: BaseMedicalPDA @@ -456,7 +468,7 @@ state: pda-library - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: LawyerPDA name: lawyer PDA description: For lawyers to poach dubious clients. @@ -499,7 +511,7 @@ state: pda-janitor - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: CaptainPDA name: captain PDA description: Surprisingly no different from your PDA. @@ -674,7 +686,7 @@ state: pda-science - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: HoSPDA name: head of security PDA description: Whosoever bears this PDA is the law. @@ -689,7 +701,7 @@ state: pda-hos - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: WardenPDA name: warden PDA description: The OS appears to have been jailbroken. @@ -704,7 +716,7 @@ state: pda-warden - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: SecurityPDA name: security PDA description: Red to hide the stains of passenger blood. @@ -718,7 +730,7 @@ state: pda-security - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: CentcomPDA name: CentComm PDA description: Light green sign of walking bureaucracy. @@ -756,6 +768,7 @@ - NotekeeperCartridge - NewsReaderCartridge - LogProbeCartridge + - WantedListCartridge - type: entity parent: CentcomPDA @@ -857,7 +870,7 @@ - Cartridge - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: ERTLeaderPDA name: ERT Leader PDA suffix: Leader @@ -1005,7 +1018,7 @@ state: pda-boxer - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: DetectivePDA name: detective PDA description: Smells like rain... pouring down the rooftops... @@ -1105,7 +1118,7 @@ state: pda-seniorphysician - type: entity - parent: BasePDA + parent: [BaseSecurityPDA, BasePDA] id: SeniorOfficerPDA name: senior officer PDA description: Beaten, battered and broken, but just barely useable. diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml index 6f925139fb1bd8..f1172c5de08ca9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml @@ -106,3 +106,56 @@ - type: Construction graph: ImprovisedArrow node: ImprovisedArrow + +- type: entity + parent: BaseArrow + id: ArrowImprovisedPlasma + name: plasma glass shard arrow + description: The greyshirt's preferred projectile. Now with extra lethality! + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/arrows.rsi + layers: + - state: tail + color: white + - state: rod + color: darkgray + - state: tip + color: purple + - state: solution1 + map: ["enum.SolutionContainerLayers.Fill"] + visible: false + - type: Projectile + damage: + types: + Piercing: 30 + - type: Construction + graph: ImprovisedArrowPlasma + node: ImprovisedArrowPlasma + +- type: entity + parent: BaseArrow + id: ArrowImprovisedUranium + name: uranium glass shard arrow + description: The greyshirt's preferred projectile. Now with added radiation! + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/arrows.rsi + layers: + - state: tail + color: white + - state: rod + color: darkgray + - state: tip + color: green + - state: solution1 + map: ["enum.SolutionContainerLayers.Fill"] + visible: false + - type: Projectile + damage: + types: + Piercing: 25 + Radiation: 5 + - type: Construction + graph: ImprovisedArrowUranium + node: ImprovisedArrowUranium diff --git a/Resources/Prototypes/Entities/Structures/Machines/recycler.yml b/Resources/Prototypes/Entities/Structures/Machines/recycler.yml index 8bbbad6c0d1966..f62923fe2af385 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/recycler.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/recycler.yml @@ -85,6 +85,7 @@ volume: -3 cutOffSound: false - type: MaterialStorage + insertOnInteract: false - type: Conveyor - type: Rotatable - type: Repairable diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 1cc26c067da1a9..82d1220a427f10 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -74,6 +74,7 @@ ForensicScannerStealObjective: 1 #sec FlippoEngravedLighterStealObjective: 0.5 ClothingHeadHatWardenStealObjective: 1 + WantedListCartridgeStealObjective: 1 ClothingOuterHardsuitVoidParamedStealObjective: 1 #med MedicalTechFabCircuitboardStealObjective: 1 ClothingHeadsetAltMedicalStealObjective: 1 diff --git a/Resources/Prototypes/Objectives/stealTargetGroups.yml b/Resources/Prototypes/Objectives/stealTargetGroups.yml index 48c871eb469766..7216307c326e89 100644 --- a/Resources/Prototypes/Objectives/stealTargetGroups.yml +++ b/Resources/Prototypes/Objectives/stealTargetGroups.yml @@ -263,6 +263,13 @@ sprite: Clothing/Neck/Medals/clownmedal.rsi state: icon +- type: stealTargetGroup + id: WantedListCartridge + name: steal-target-groups-wanted-list-cartridge + sprite: + sprite: Objects/Devices/cartridge.rsi + state: cart-sec + #Thief structures - type: stealTargetGroup diff --git a/Resources/Prototypes/Objectives/thief.yml b/Resources/Prototypes/Objectives/thief.yml index 970a5058ecbde6..014b59d8b007e1 100644 --- a/Resources/Prototypes/Objectives/thief.yml +++ b/Resources/Prototypes/Objectives/thief.yml @@ -184,6 +184,15 @@ - type: Objective difficulty: 1.2 +- type: entity + parent: BaseThiefStealObjective + id: WantedListCartridgeStealObjective + components: + - type: StealCondition + stealGroup: WantedListCartridge + - type: Objective + difficulty: 1 + - type: entity #Medical subgroup parent: BaseThiefStealObjective id: ClothingOuterHardsuitVoidParamedStealObjective diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 330b2e361c2a6e..016ca06bf6c091 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -369,6 +369,10 @@ key: KnockedDown time: 0.75 type: Remove + - !type:GenericStatusEffect + key: Adrenaline # Guess what epinephrine is... + component: IgnoreSlowOnDamage + time: 5 # lingers less than hivemind reagent to give it a niche - type: reagent id: Hyronalin diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/weapons/improvised_arrow.yml b/Resources/Prototypes/Recipes/Construction/Graphs/weapons/improvised_arrow.yml index 04b94690467dac..20e06e9f48412c 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/weapons/improvised_arrow.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/weapons/improvised_arrow.yml @@ -21,3 +21,51 @@ - node: ImprovisedArrow entity: ArrowImprovised + +- type: constructionGraph + id: ImprovisedArrowPlasma + start: start + graph: + - node: start + edges: + - to: ImprovisedArrowPlasma + steps: + - material: MetalRod + amount: 1 + doAfter: 0.5 + - material: Cloth + amount: 1 + doAfter: 0.5 + - tag: PlasmaGlassShard + name: plasma glass shard + icon: + sprite: Objects/Materials/Shards/shard.rsi + state: shard1 + doAfter: 0.5 + + - node: ImprovisedArrowPlasma + entity: ArrowImprovisedPlasma + +- type: constructionGraph + id: ImprovisedArrowUranium + start: start + graph: + - node: start + edges: + - to: ImprovisedArrowUranium + steps: + - material: MetalRod + amount: 1 + doAfter: 0.5 + - material: Cloth + amount: 1 + doAfter: 0.5 + - tag: UraniumGlassShard + name: uranium glass shard + icon: + sprite: Objects/Materials/Shards/shard.rsi + state: shard1 + doAfter: 0.5 + + - node: ImprovisedArrowUranium + entity: ArrowImprovisedUranium diff --git a/Resources/Prototypes/Recipes/Construction/weapons.yml b/Resources/Prototypes/Recipes/Construction/weapons.yml index 040d4c8963d138..5936a350691325 100644 --- a/Resources/Prototypes/Recipes/Construction/weapons.yml +++ b/Resources/Prototypes/Recipes/Construction/weapons.yml @@ -152,6 +152,28 @@ icon: { sprite: Objects/Weapons/Guns/Bow/bow.rsi, state: wielded-arrow } objectType: Item +- type: construction + name: plasma glass shard arrow + id: ImprovisedArrowPlasma + graph: ImprovisedArrowPlasma + startNode: start + targetNode: ImprovisedArrowPlasma + category: construction-category-weapons + description: An arrow tipped with pieces of a plasma glass shard, for use with a bow. + icon: { sprite: Objects/Weapons/Guns/Bow/bow.rsi, state: wielded-arrow } + objectType: Item + +- type: construction + name: uranium glass shard arrow + id: ImprovisedArrowUranium + graph: ImprovisedArrowUranium + startNode: start + targetNode: ImprovisedArrowUranium + category: construction-category-weapons + description: An arrow tipped with pieces of a uranium glass shard, for use with a bow. + icon: { sprite: Objects/Weapons/Guns/Bow/bow.rsi, state: wielded-arrow } + objectType: Item + - type: construction name: improvised bow id: ImprovisedBow diff --git a/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml b/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml index 3eb9c2ca2f2b0f..7e5e20139b09a1 100644 --- a/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml +++ b/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml @@ -13,7 +13,7 @@ WARNING: This is not an automatically generated list, things here may become out - Tortila Dough = 15 Cornmeal, 10 Water - Tofu = 5 Enzyme (Catalyst), 30 Soy Milk - Pie Dough = 2 Eggs (12u), 15 Flour, 5 Table Salt -- Cake Batter = 2 Eggs(12u), 15 flour, 5 Sugar +- Cake Batter = 2 Eggs(12u), 15 flour, 5 Sugar, 5 Milk - Vegan Cake Batter = 15 Soy Milk, 15 Flour, 5 Sugar - Butter = 30 Milk, 5 Table Salt (Catalyst) - Cheese Wheel = 5 Enzyme (Catalyst), 40 Milk diff --git a/Resources/Textures/Decals/burnt.rsi/burnt1.png b/Resources/Textures/Decals/burnt.rsi/burnt1.png new file mode 100644 index 00000000000000..3fcb7a4949b5bd Binary files /dev/null and b/Resources/Textures/Decals/burnt.rsi/burnt1.png differ diff --git a/Resources/Textures/Decals/burnt.rsi/burnt2.png b/Resources/Textures/Decals/burnt.rsi/burnt2.png new file mode 100644 index 00000000000000..01f8f220b2ddae Binary files /dev/null and b/Resources/Textures/Decals/burnt.rsi/burnt2.png differ diff --git a/Resources/Textures/Decals/burnt.rsi/burnt3.png b/Resources/Textures/Decals/burnt.rsi/burnt3.png new file mode 100644 index 00000000000000..e9dcbe37533f6e Binary files /dev/null and b/Resources/Textures/Decals/burnt.rsi/burnt3.png differ diff --git a/Resources/Textures/Decals/burnt.rsi/burnt4.png b/Resources/Textures/Decals/burnt.rsi/burnt4.png new file mode 100644 index 00000000000000..f1db0637ccbdbf Binary files /dev/null and b/Resources/Textures/Decals/burnt.rsi/burnt4.png differ diff --git a/Resources/Textures/Decals/burnt.rsi/meta.json b/Resources/Textures/Decals/burnt.rsi/meta.json new file mode 100644 index 00000000000000..48a8f33138cfef --- /dev/null +++ b/Resources/Textures/Decals/burnt.rsi/meta.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "From https://github.com/BeeStation/BeeStation-Hornet/blob/master/icons/turf/turf_damage.dmi at f2d6fbdf36aa0951049498cf28e028a38e32fe0b", + "states": [ + { + "name": "burnt1" + + }, + { + "name": "burnt2" + + }, + { + "name": "burnt3" + + }, + { + "name": "burnt4" + + } + ] +} diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png new file mode 100644 index 00000000000000..6a3197004cbf04 Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png differ diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json index f3b02a2b2eab4c..d5fad560062193 100644 --- a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d and tgstation at tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8, cart-log made by Skarletto (github)", + "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d and tgstation at tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8, cart-log made by Skarletto (github), cart-sec made by dieselmohawk (discord)", "size": { "x": 32, "y": 32 @@ -79,6 +79,9 @@ { "name": "cart-y" }, + { + "name": "cart-sec" + }, { "name": "insert_overlay" } diff --git a/Tools/publish_multi_request.py b/Tools/publish_multi_request.py new file mode 100755 index 00000000000000..a63359afd6c424 --- /dev/null +++ b/Tools/publish_multi_request.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import requests +import os +import subprocess +from typing import Iterable + +PUBLISH_TOKEN = os.environ["PUBLISH_TOKEN"] +VERSION = os.environ["GITHUB_SHA"] + +RELEASE_DIR = "release" + +# +# CONFIGURATION PARAMETERS +# Forks should change these to publish to their own infrastructure. +# +ROBUST_CDN_URL = "https://wizards.cdn.spacestation14.com/" +FORK_ID = "wizards" + +def main(): + session = requests.Session() + session.headers = { + "Authorization": f"Bearer {PUBLISH_TOKEN}", + } + + print(f"Starting publish on Robust.Cdn for version {VERSION}") + + data = { + "version": VERSION, + "engineVersion": get_engine_version(), + } + headers = { + "Content-Type": "application/json" + } + resp = session.post(f"{ROBUST_CDN_URL}fork/{FORK_ID}/publish/start", json=data, headers=headers) + resp.raise_for_status() + print("Publish successfully started, adding files...") + + for file in get_files_to_publish(): + print(f"Publishing {file}") + with open(file, "rb") as f: + headers = { + "Content-Type": "application/octet-stream", + "Robust-Cdn-Publish-File": os.path.basename(file), + "Robust-Cdn-Publish-Version": VERSION + } + resp = session.post(f"{ROBUST_CDN_URL}fork/{FORK_ID}/publish/file", data=f, headers=headers) + + resp.raise_for_status() + + print("Successfully pushed files, finishing publish...") + + data = { + "version": VERSION + } + headers = { + "Content-Type": "application/json" + } + resp = session.post(f"{ROBUST_CDN_URL}fork/{FORK_ID}/publish/finish", json=data, headers=headers) + resp.raise_for_status() + + print("SUCCESS!") + + +def get_files_to_publish() -> Iterable[str]: + for file in os.listdir(RELEASE_DIR): + yield os.path.join(RELEASE_DIR, file) + + +def get_engine_version() -> str: + proc = subprocess.run(["git", "describe","--tags", "--abbrev=0"], stdout=subprocess.PIPE, cwd="RobustToolbox", check=True, encoding="UTF-8") + tag = proc.stdout.strip() + assert tag.startswith("v") + return tag[1:] # Cut off v prefix. + + +if __name__ == '__main__': + main()