diff --git a/.envrc b/.envrc index 5def8fd66a2..7fd05db3e5e 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,4 @@ -if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8=" +if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" fi use flake diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index eceeee03ad0..31d092b25d1 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -79,10 +79,12 @@ private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent private void BaseHandleState(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent { + // TODO ACTIONS use auto comp states component.Icon = state.Icon; component.IconOn = state.IconOn; component.IconColor = state.IconColor; - component.Keywords = new HashSet(state.Keywords); + component.Keywords.Clear(); + component.Keywords.UnionWith(state.Keywords); component.Enabled = state.Enabled; component.Toggled = state.Toggled; component.Cooldown = state.Cooldown; @@ -102,8 +104,7 @@ private void BaseHandleState(EntityUid uid, BaseActionComponent component, Ba component.ItemIconStyle = state.ItemIconStyle; component.Sound = state.Sound; - if (_playerManager.LocalEntity == component.AttachedEntity) - ActionsUpdated?.Invoke(); + UpdateAction(uid, component); } protected override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null) diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index 41c3ac76f98..fdf935d7c04 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -29,6 +29,8 @@ public sealed partial class PlayerListControl : BoxContainer private IEntityManager _entManager; private IUserInterfaceManager _uiManager; + private PlayerInfo? _selectedPlayer; + public PlayerListControl() { _entManager = IoCManager.Resolve(); @@ -50,10 +52,14 @@ private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? d if (args == null || data is not PlayerListData {Info: var selectedPlayer}) return; + if (selectedPlayer == _selectedPlayer) + return; + if (args.Event.Function != EngineKeyFunctions.UIClick) return; OnSelectionChanged?.Invoke(selectedPlayer); + _selectedPlayer = selectedPlayer; // update label text. Only required if there is some override (e.g. unread bwoink count). if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label) @@ -95,6 +101,8 @@ private void FilterList() _sortedPlayerList.Sort((a, b) => Comparison(a, b)); PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList()); + if (_selectedPlayer != null) + PlayerListContainer.Select(new PlayerListData(_selectedPlayer)); } public void PopulateList(IReadOnlyList? players = null) @@ -102,6 +110,9 @@ public void PopulateList(IReadOnlyList? players = null) players ??= _adminSystem.PlayerList; _playerList = players.ToList(); + if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer)) + _selectedPlayer = null; + FilterList(); } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs index e70248880b0..ead1d8b00e5 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs @@ -68,7 +68,7 @@ private void Refresh() SeverityRect.Texture = _sprites.Frame0(new SpriteSpecifier.Texture(new ResPath(iconPath))); } - TimeLabel.Text = Note.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss"); + TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); ServerLabel.Text = Note.ServerName ?? "Unknown"; RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round; AdminLabel.Text = Note.CreatedByName; @@ -91,7 +91,7 @@ private void Refresh() if (Note.ExpiryTime.Value > DateTime.UtcNow) { ExpiresLabel.Text = Loc.GetString("admin-note-editor-expiry-label-params", - ("date", Note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss")), + ("date", Note.ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")), ("expiresIn", (Note.ExpiryTime.Value - DateTime.UtcNow).ToString("d'd 'hh':'mm"))); ExpiresLabel.Modulate = Color.FromHex("#86DC3D"); } @@ -104,7 +104,7 @@ private void Refresh() if (Note.LastEditedAt > Note.CreatedAt) { - EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt)); + EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt.Value.ToLocalTime())); EditedLabel.Visible = true; } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs index 5ef29513e24..18a50031582 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs @@ -36,12 +36,12 @@ public AdminNotesLinePopup(SharedAdminNote note, string playerName, bool showDel ? Loc.GetString("admin-notes-round-id-unknown") : Loc.GetString("admin-notes-round-id", ("id", note.Round)); CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName)); - CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss"))); + CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName)); - EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? Loc.GetString("admin-notes-edited-never"))); + EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? Loc.GetString("admin-notes-edited-never"))); ExpiryTimeLabel.Text = note.ExpiryTime == null ? Loc.GetString("admin-notes-expires-never") - : Loc.GetString("admin-notes-expires", ("expires", note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss"))); + : Loc.GetString("admin-notes-expires", ("expires", note.ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); NoteTextEdit.InsertAtCursor(note.Message); if (note.NoteType is NoteType.ServerBan or NoteType.RoleBan) diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs index 77dde4688d2..6f314f79542 100644 --- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs +++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs @@ -81,7 +81,7 @@ public NoteEdit(SharedAdminNote? note, string playerName, bool canCreate, bool c { PermanentCheckBox.Pressed = false; UpdatePermanentCheckboxFields(); - ExpiryLineEdit.Text = ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); + ExpiryLineEdit.Text = ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); } } @@ -173,7 +173,7 @@ private void UpdatePermanentCheckboxFields() ExpiryLabel.Visible = !PermanentCheckBox.Pressed; ExpiryLineEdit.Visible = !PermanentCheckBox.Pressed; - ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty; + ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty; } private void OnSecretPressed(BaseButton.ButtonEventArgs _) @@ -269,7 +269,7 @@ private bool ParseExpiryTime() return false; } - ExpiryTime = result; + ExpiryTime = result.ToUniversalTime(); ExpiryLineEdit.ModulateSelfOverride = null; return true; } diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs index b4486b8c0ee..f1e8e8d7aa4 100644 --- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs +++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs @@ -49,12 +49,17 @@ protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisua if (!args.Sprite.LayerMapTryGet(component.Layer, out var fillLayer)) return; + var maxFillLevels = component.MaxFillLevels; + var fillBaseName = component.FillBaseName; + var changeColor = component.ChangeColor; + var fillSprite = component.MetamorphicDefaultSprite; + // Currently some solution methods such as overflowing will try to update appearance with a // volume greater than the max volume. We'll clamp it so players don't see // a giant error sign and error for debug. if (fraction > 1f) { - Logger.Error("Attempted to set solution container visuals volume ratio on " + ToPrettyString(uid) + " to a value greater than 1. Volume should never be greater than max volume!"); + Log.Error("Attempted to set solution container visuals volume ratio on " + ToPrettyString(uid) + " to a value greater than 1. Volume should never be greater than max volume!"); fraction = 1f; } if (component.Metamorphic) @@ -72,13 +77,23 @@ protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisua if (reagentProto?.MetamorphicSprite is { } sprite) { args.Sprite.LayerSetSprite(baseLayer, sprite); - args.Sprite.LayerSetVisible(fillLayer, false); + if (reagentProto.MetamorphicMaxFillLevels > 0) + { + args.Sprite.LayerSetVisible(fillLayer, true); + maxFillLevels = reagentProto.MetamorphicMaxFillLevels; + fillBaseName = reagentProto.MetamorphicFillBaseName; + changeColor = reagentProto.MetamorphicChangeColor; + fillSprite = sprite; + } + else + args.Sprite.LayerSetVisible(fillLayer, false); + if (hasOverlay) args.Sprite.LayerSetVisible(overlayLayer, false); - return; } else { + args.Sprite.LayerSetVisible(fillLayer, true); if (hasOverlay) args.Sprite.LayerSetVisible(overlayLayer, true); if (component.MetamorphicDefaultSprite != null) @@ -87,21 +102,27 @@ protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisua } } } + else + { + args.Sprite.LayerSetVisible(fillLayer, true); + } - int closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, component.MaxFillLevels + 1); + var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1); if (closestFillSprite > 0) { - if (component.FillBaseName == null) + if (fillBaseName == null) return; - args.Sprite.LayerSetVisible(fillLayer, true); - - var stateName = component.FillBaseName + closestFillSprite; + var stateName = fillBaseName + closestFillSprite; + if (fillSprite != null) + args.Sprite.LayerSetSprite(fillLayer, fillSprite); args.Sprite.LayerSetState(fillLayer, stateName); - if (component.ChangeColor && AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.Color, out var color, args.Component)) + if (changeColor && AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.Color, out var color, args.Component)) args.Sprite.LayerSetColor(fillLayer, color); + else + args.Sprite.LayerSetColor(fillLayer, Color.White); } else { @@ -110,8 +131,10 @@ protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisua else { args.Sprite.LayerSetState(fillLayer, component.EmptySpriteName); - if (component.ChangeColor) + if (changeColor) args.Sprite.LayerSetColor(fillLayer, component.EmptySpriteColor); + else + args.Sprite.LayerSetColor(fillLayer, Color.White); } } @@ -130,7 +153,7 @@ private void OnGetHeldVisuals(EntityUid uid, SolutionContainerVisualsComponent c if (!AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.FillFraction, out var fraction, appearance)) return; - int closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, component.InHandsMaxFillLevels + 1); + var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, component.InHandsMaxFillLevels + 1); if (closestFillSprite > 0) { diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs index 0d13baef936..1cfb4c2a558 100644 --- a/Content.Client/StatusIcon/StatusIconOverlay.cs +++ b/Content.Client/StatusIcon/StatusIconOverlay.cs @@ -5,6 +5,7 @@ using Robust.Shared.Enums; using System.Numerics; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; namespace Content.Client.StatusIcon; @@ -12,6 +13,7 @@ public sealed class StatusIconOverlay : Overlay { [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IGameTiming _timing = default!; private readonly SpriteSystem _sprite; private readonly TransformSystem _transform; @@ -72,7 +74,9 @@ protected override void Draw(in OverlayDrawArgs args) foreach (var proto in icons) { - var texture = _sprite.Frame0(proto.Icon); + + var curTime = _timing.RealTime; + var texture = _sprite.GetFrame(proto.Icon, curTime); float yOffset; float xOffset; diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml index a990c4eff64..1fcb1a7898c 100644 --- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml +++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml @@ -1,10 +1,28 @@ - + - + + ItemSeparation="2" + Margin="4 0" + SelectMode="Button" + StyleClasses="transparentBackgroundItemList"> + + + + + + - + diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs index a2741cc17ab..8b53290f7fb 100644 --- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs +++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs @@ -4,14 +4,15 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; +using Content.Client.Stylesheets; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow; namespace Content.Client.VendingMachines.UI { [GenerateTypedNameReferences] - public sealed partial class VendingMachineMenu : DefaultWindow + public sealed partial class VendingMachineMenu : FancyWindow { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; diff --git a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs new file mode 100644 index 00000000000..420a90a50bd --- /dev/null +++ b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Content.Shared.Actions; +using Content.Shared.Eye; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Actions; + +[TestFixture] +public sealed class ActionPvsDetachTest +{ + [Test] + public async Task TestActionDetach() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var (server, client) = pair; + var sys = server.System(); + var cSys = client.System(); + + // Spawn mob that has some actions + EntityUid ent = default; + var map = await pair.CreateTestMap(); + await server.WaitPost(() => ent = server.EntMan.SpawnAtPosition("MobHuman", map.GridCoords)); + await pair.RunTicksSync(5); + var cEnt = pair.ToClientUid(ent); + + // Verify that both the client & server agree on the number of actions + var initActions = sys.GetActions(ent).Count(); + Assert.That(initActions, Is.GreaterThan(0)); + Assert.That(initActions, Is.EqualTo(cSys.GetActions(cEnt).Count())); + + // PVS-detach action entities + // We do this by just giving them the ghost layer + var visSys = server.System(); + var enumerator = server.Transform(ent).ChildEnumerator; + while (enumerator.MoveNext(out var child)) + { + visSys.AddLayer(child, (int) VisibilityFlags.Ghost); + } + await pair.RunTicksSync(5); + + // Client's actions have left been detached / are out of view, but action comp state has not changed + Assert.That(sys.GetActions(ent).Count(), Is.EqualTo(initActions)); + Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions)); + + // Re-enter PVS view + enumerator = server.Transform(ent).ChildEnumerator; + while (enumerator.MoveNext(out var child)) + { + visSys.RemoveLayer(child, (int) VisibilityFlags.Ghost); + } + await pair.RunTicksSync(5); + Assert.That(sys.GetActions(ent).Count(), Is.EqualTo(initActions)); + Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions)); + + await server.WaitPost(() => server.EntMan.DeleteEntity(map.MapUid)); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 551b5108b11..def6afebeb6 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -45,22 +45,7 @@ public sealed class PostMapInitTest { "Dev", "TestTeg", - //"Fland", - //"Meta", - //"Packed", - //"Aspid", - //"Cluster", - //"Omega", - //"Bagel", - //"Origin", "CentComm", - //"Box", - //"Europa", - //"Barratry", - //"Saltern", - //"Core", - //"Marathon", - //"Kettle", "MeteorArena", "Pebble", //DeltaV "Edge", //DeltaV diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index d6dec1dc3e1..f9ec7811f5d 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -874,33 +874,8 @@ public sealed class UploadedResourceLog public byte[] Data { get; set; } = default!; } - public interface IAdminRemarksCommon - { - public int Id { get; } - - public int? RoundId { get; } - public Round? Round { get; } - - public Guid? PlayerUserId { get; } - public Player? Player { get; } - public TimeSpan PlaytimeAtNote { get; } - - public string Message { get; } - - public Player? CreatedBy { get; } - - public DateTime CreatedAt { get; } - - public Player? LastEditedBy { get; } - - public DateTime? LastEditedAt { get; } - public DateTime? ExpirationTime { get; } - - public bool Deleted { get; } - } - [Index(nameof(PlayerUserId))] - public class AdminNote : IAdminRemarksCommon + public class AdminNote { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } @@ -934,7 +909,7 @@ public class AdminNote : IAdminRemarksCommon } [Index(nameof(PlayerUserId))] - public class AdminWatchlist : IAdminRemarksCommon + public class AdminWatchlist { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } @@ -965,7 +940,7 @@ public class AdminWatchlist : IAdminRemarksCommon } [Index(nameof(PlayerUserId))] - public class AdminMessage : IAdminRemarksCommon + public class AdminMessage { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index a6b1856ab17..7499d0b0f59 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -10,11 +10,6 @@ namespace Content.Server.Database { public sealed class PostgresServerDbContext : ServerDbContext { - static PostgresServerDbContext() - { - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); - } - public PostgresServerDbContext(DbContextOptions options) : base(options) { } diff --git a/Content.Server/Administration/Notes/AdminMessageEui.cs b/Content.Server/Administration/Notes/AdminMessageEui.cs index ddb91aca7c6..c5e0b60172c 100644 --- a/Content.Server/Administration/Notes/AdminMessageEui.cs +++ b/Content.Server/Administration/Notes/AdminMessageEui.cs @@ -13,7 +13,7 @@ public sealed class AdminMessageEui : BaseEui [Dependency] private readonly IAdminNotesManager _notesMan = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; private readonly float _closeWait; - private AdminMessage? _message; + private AdminMessageRecord? _message; private DateTime _startTime; public AdminMessageEui() @@ -22,7 +22,7 @@ public AdminMessageEui() _closeWait = _cfg.GetCVar(CCVars.MessageWaitTime); } - public void SetMessage(AdminMessage message) + public void SetMessage(AdminMessageRecord message) { _message = message; _startTime = DateTime.UtcNow; @@ -37,7 +37,7 @@ public override EuiStateBase GetNewState() _closeWait, _message.Message, _message.CreatedBy?.LastSeenUserName ?? "[System]", - _message.CreatedAt + _message.CreatedAt.UtcDateTime ); } diff --git a/Content.Server/Administration/Notes/AdminNotesExtensions.cs b/Content.Server/Administration/Notes/AdminNotesExtensions.cs index 44ad20eec67..349c7ff3bdf 100644 --- a/Content.Server/Administration/Notes/AdminNotesExtensions.cs +++ b/Content.Server/Administration/Notes/AdminNotesExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; @@ -7,7 +6,7 @@ namespace Content.Server.Administration.Notes; public static class AdminNotesExtensions { - public static SharedAdminNote ToShared(this IAdminRemarksCommon note) + public static SharedAdminNote ToShared(this IAdminRemarksRecord note) { NoteSeverity? severity = null; var secret = false; @@ -18,26 +17,26 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) bool? seen = null; switch (note) { - case AdminNote adminNote: + case AdminNoteRecord adminNote: type = NoteType.Note; severity = adminNote.Severity; secret = adminNote.Secret; break; - case AdminWatchlist: + case AdminWatchlistRecord: type = NoteType.Watchlist; secret = true; break; - case AdminMessage adminMessage: + case AdminMessageRecord adminMessage: type = NoteType.Message; seen = adminMessage.Seen; break; - case ServerBanNote ban: + case ServerBanNoteRecord ban: type = NoteType.ServerBan; severity = ban.Severity; unbannedTime = ban.UnbanTime; unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user"); break; - case ServerRoleBanNote roleBan: + case ServerRoleBanNoteRecord roleBan: type = NoteType.RoleBan; severity = roleBan.Severity; bannedRoles = roleBan.Roles; @@ -49,12 +48,13 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) } // There may be bans without a user, but why would we ever be converting them to shared notes? - if (note.PlayerUserId is null) - throw new ArgumentNullException(nameof(note.PlayerUserId), "Player user ID cannot be null for a note"); + if (note.Player is null) + throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); + return new SharedAdminNote( note.Id, - note.PlayerUserId.Value, - note.RoundId, + note.Player!.UserId, + note.Round?.Id, note.Round?.Server.Name, note.PlaytimeAtNote, type, @@ -63,9 +63,9 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) secret, note.CreatedBy?.LastSeenUserName ?? Loc.GetString("system-user"), note.LastEditedBy?.LastSeenUserName ?? string.Empty, - note.CreatedAt, - note.LastEditedAt, - note.ExpirationTime, + note.CreatedAt.UtcDateTime, + note.LastEditedAt?.UtcDateTime, + note.ExpirationTime?.UtcDateTime, bannedRoles, unbannedTime, unbannedByName, diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 0c1e7f3daad..e09e1906486 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -144,7 +144,7 @@ public async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType var note = new SharedAdminNote( noteId, - player, + (NetUserId) player, roundId, serverName, playtime, @@ -306,27 +306,27 @@ public async Task ModifyAdminRemark(int noteId, NoteType type, ICommonSession ed NoteModified?.Invoke(newNote); } - public async Task> GetAllAdminRemarks(Guid player) + public async Task> GetAllAdminRemarks(Guid player) { return await _db.GetAllAdminRemarks(player); } - public async Task> GetVisibleRemarks(Guid player) + public async Task> GetVisibleRemarks(Guid player) { if (_config.GetCVar(CCVars.SeeOwnNotes)) { return await _db.GetVisibleAdminNotes(player); } _sawmill.Warning($"Someone tried to call GetVisibleNotes for {player} when see_own_notes was false"); - return new List(); + return new List(); } - public async Task> GetActiveWatchlists(Guid player) + public async Task> GetActiveWatchlists(Guid player) { return await _db.GetActiveWatchlists(player); } - public async Task> GetNewMessages(Guid player) + public async Task> GetNewMessages(Guid player) { return await _db.GetMessages(player); } diff --git a/Content.Server/Administration/Notes/IAdminNotesManager.cs b/Content.Server/Administration/Notes/IAdminNotesManager.cs index a726bd11c82..81ebd3e7166 100644 --- a/Content.Server/Administration/Notes/IAdminNotesManager.cs +++ b/Content.Server/Administration/Notes/IAdminNotesManager.cs @@ -26,24 +26,24 @@ public interface IAdminNotesManager /// /// Desired player's /// ALL non-deleted notes, secret or not - Task> GetAllAdminRemarks(Guid player); + Task> GetAllAdminRemarks(Guid player); /// /// Queries the database and retrieves the notes a player should see /// /// Desired player's /// All player-visible notes - Task> GetVisibleRemarks(Guid player); + Task> GetVisibleRemarks(Guid player); /// /// Queries the database and retrieves watchlists that may have been placed on the player /// /// Desired player's /// Active watchlists - Task> GetActiveWatchlists(Guid player); + Task> GetActiveWatchlists(Guid player); /// /// Queries the database and retrieves new messages a player has gotten /// /// Desired player's /// All unread messages - Task> GetNewMessages(Guid player); + Task> GetNewMessages(Guid player); Task MarkMessageAsSeen(int id); } diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs new file mode 100644 index 00000000000..af740a4d74c --- /dev/null +++ b/Content.Server/Database/DatabaseRecords.cs @@ -0,0 +1,127 @@ +using System.Collections.Immutable; +using System.Net; +using Content.Shared.Database; +using Robust.Shared.Network; + +namespace Content.Server.Database; + +// This file contains copies of records returned from the database. +// We can't return the raw EF Core entities as they are often unsuited. +// (e.g. datetime handling of Microsoft.Data.Sqlite) + +public interface IAdminRemarksRecord +{ + public int Id { get; } + + public RoundRecord? Round { get; } + + public PlayerRecord? Player { get; } + public TimeSpan PlaytimeAtNote { get; } + + public string Message { get; } + + public PlayerRecord? CreatedBy { get; } + + public DateTimeOffset CreatedAt { get; } + + public PlayerRecord? LastEditedBy { get; } + + public DateTimeOffset? LastEditedAt { get; } + public DateTimeOffset? ExpirationTime { get; } + + public bool Deleted { get; } +} + +public sealed record ServerRoleBanNoteRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + NoteSeverity Severity, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + string[] Roles, + PlayerRecord? UnbanningAdmin, + DateTime? UnbanTime) : IAdminRemarksRecord; + +public sealed record ServerBanNoteRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + NoteSeverity Severity, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? UnbanningAdmin, + DateTime? UnbanTime) : IAdminRemarksRecord; + +public sealed record AdminNoteRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + NoteSeverity Severity, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt, + bool Secret) : IAdminRemarksRecord; + +public sealed record AdminWatchlistRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt) : IAdminRemarksRecord; + +public sealed record AdminMessageRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt, + bool Seen) : IAdminRemarksRecord; + + +public sealed record PlayerRecord( + NetUserId UserId, + DateTimeOffset FirstSeenTime, + string LastSeenUserName, + DateTimeOffset LastSeenTime, + IPAddress LastSeenAddress, + ImmutableArray? HWId); + +public sealed record RoundRecord(int Id, DateTimeOffset StartDate, ServerRecord Server); + +public sealed record ServerRecord(int Id, string Name); diff --git a/Content.Server/Database/PlayerRecord.cs b/Content.Server/Database/PlayerRecord.cs deleted file mode 100644 index cfcebe1c02c..00000000000 --- a/Content.Server/Database/PlayerRecord.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Immutable; -using System.Net; -using Robust.Shared.Network; - -namespace Content.Server.Database -{ - public sealed class PlayerRecord - { - public NetUserId UserId { get; } - public ImmutableArray? HWId { get; } - public DateTimeOffset FirstSeenTime { get; } - public string LastSeenUserName { get; } - public DateTimeOffset LastSeenTime { get; } - public IPAddress LastSeenAddress { get; } - - public PlayerRecord( - NetUserId userId, - DateTimeOffset firstSeenTime, - string lastSeenUserName, - DateTimeOffset lastSeenTime, - IPAddress lastSeenAddress, - ImmutableArray? hwId) - { - UserId = userId; - FirstSeenTime = firstSeenTime; - LastSeenUserName = lastSeenUserName; - LastSeenTime = lastSeenTime; - LastSeenAddress = lastSeenAddress; - HWId = hwId; - } - } -} diff --git a/Content.Server/Database/ServerBanNote.cs b/Content.Server/Database/ServerBanNote.cs deleted file mode 100644 index 4e556500900..00000000000 --- a/Content.Server/Database/ServerBanNote.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Content.Shared.Database; - -namespace Content.Server.Database -{ - public record ServerBanNote(int Id, int? RoundId, Round? Round, Guid? PlayerUserId, Player? Player, - TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, Player? CreatedBy, DateTime CreatedAt, - Player? LastEditedBy, DateTime? LastEditedAt, DateTime? ExpirationTime, bool Deleted, Player? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksCommon; -} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 27ccb6ee0ea..fe42d73ae95 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -356,7 +357,7 @@ public abstract Task> GetServerBansAsync( public abstract Task AddServerBanAsync(ServerBanDef serverBan); public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); - public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -365,9 +366,9 @@ public async Task EditServerBan(int id, string reason, NoteSeverity severity, Da return; ban.Severity = severity; ban.Reason = reason; - ban.ExpirationTime = expiration; + ban.ExpirationTime = expiration?.UtcDateTime; ban.LastEditedById = editedBy; - ban.LastEditedAt = editedAt; + ban.LastEditedAt = editedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } @@ -448,7 +449,7 @@ public abstract Task> GetServerRoleBansAsync(IPAddress? a public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban); - public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -457,9 +458,9 @@ public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity return; ban.Severity = severity; ban.Reason = reason; - ban.ExpirationTime = expiration; + ban.ExpirationTime = expiration?.UtcDateTime; ban.LastEditedById = editedBy; - ban.LastEditedAt = editedAt; + ban.LastEditedAt = editedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } #endregion @@ -571,7 +572,21 @@ public async Task UpdatePlayerRecord( return record == null ? null : MakePlayerRecord(record); } - protected abstract PlayerRecord MakePlayerRecord(Player player); + [return: NotNullIfNotNull(nameof(player))] + protected PlayerRecord? MakePlayerRecord(Player? player) + { + if (player == null) + return null; + + return new PlayerRecord( + new NetUserId(player.UserId), + new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)), + player.LastSeenUserName, + new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)), + player.LastSeenAddress, + player.LastSeenHWId?.ToImmutableArray()); + } + #endregion #region Connection Logs @@ -733,6 +748,18 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} await db.DbContext.SaveChangesAsync(); } + [return: NotNullIfNotNull(nameof(round))] + protected RoundRecord? MakeRoundRecord(Round? round) + { + if (round == null) + return null; + + return new RoundRecord( + round.Id, + NormalizeDatabaseTime(round.StartDate), + MakeServerRecord(round.Server)); + } + public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) { await using var db = await GetDb(); @@ -772,6 +799,15 @@ public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) return (server, false); } + [return: NotNullIfNotNull(nameof(server))] + protected ServerRecord? MakeServerRecord(Server? server) + { + if (server == null) + return null; + + return new ServerRecord(server.Id, server.Name); + } + public async Task AddAdminLogs(List logs) { DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids."); @@ -943,17 +979,17 @@ public async Task RemoveFromWhitelistAsync(NetUserId player) await db.DbContext.SaveChangesAsync(); } - public async Task GetLastReadRules(NetUserId player) + public async Task GetLastReadRules(NetUserId player) { await using var db = await GetDb(); - return await db.DbContext.Player + return NormalizeDatabaseTime(await db.DbContext.Player .Where(dbPlayer => dbPlayer.UserId == player) .Select(dbPlayer => dbPlayer.LastReadRules) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync()); } - public async Task SetLastReadRules(NetUserId player, DateTime date) + public async Task SetLastReadRules(NetUserId player, DateTimeOffset date) { await using var db = await GetDb(); @@ -963,7 +999,7 @@ public async Task SetLastReadRules(NetUserId player, DateTime date) return; } - dbPlayer.LastReadRules = date; + dbPlayer.LastReadRules = date.UtcDateTime; await db.DbContext.SaveChangesAsync(); } @@ -971,11 +1007,11 @@ public async Task SetLastReadRules(NetUserId player, DateTime date) #region Uploaded Resources Logs - public async Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data) + public async Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data) { await using var db = await GetDb(); - db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date, Path = path, Data = data }); + db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date.UtcDateTime, Path = path, Data = data }); await db.DbContext.SaveChangesAsync(); } @@ -983,7 +1019,7 @@ public async Task PurgeUploadedResourceLogAsync(int days) { await using var db = await GetDb(); - var date = DateTime.Now.Subtract(TimeSpan.FromDays(days)); + var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(days)); await foreach (var log in db.DbContext.UploadedResourceLog .Where(l => date > l.Date) @@ -1023,10 +1059,10 @@ public virtual async Task AddAdminMessage(AdminMessage message) return message.Id; } - public async Task GetAdminNote(int id) + public async Task GetAdminNote(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminNotes + var entity = await db.DbContext.AdminNotes .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1035,12 +1071,34 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminNoteRecord(entity); } - public async Task GetAdminWatchlist(int id) + private AdminNoteRecord MakeAdminNoteRecord(AdminNote entity) + { + return new AdminNoteRecord( + entity.Id, + MakeRoundRecord(entity.Round), + MakePlayerRecord(entity.Player), + entity.PlaytimeAtNote, + entity.Message, + entity.Severity, + MakePlayerRecord(entity.CreatedBy), + NormalizeDatabaseTime(entity.CreatedAt), + MakePlayerRecord(entity.LastEditedBy), + NormalizeDatabaseTime(entity.LastEditedAt), + NormalizeDatabaseTime(entity.ExpirationTime), + entity.Deleted, + MakePlayerRecord(entity.DeletedBy), + NormalizeDatabaseTime(entity.DeletedAt), + entity.Secret); + } + + public async Task GetAdminWatchlist(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminWatchlists + var entity = await db.DbContext.AdminWatchlists .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1049,12 +1107,14 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminWatchlistRecord(entity); } - public async Task GetAdminMessage(int id) + public async Task GetAdminMessage(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminMessages + var entity = await db.DbContext.AdminMessages .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1063,9 +1123,30 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminMessageRecord(entity); } - public async Task GetServerBanAsNoteAsync(int id) + private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity) + { + return new AdminMessageRecord( + entity.Id, + MakeRoundRecord(entity.Round), + MakePlayerRecord(entity.Player), + entity.PlaytimeAtNote, + entity.Message, + MakePlayerRecord(entity.CreatedBy), + NormalizeDatabaseTime(entity.CreatedAt), + MakePlayerRecord(entity.LastEditedBy), + NormalizeDatabaseTime(entity.LastEditedAt), + NormalizeDatabaseTime(entity.ExpirationTime), + entity.Deleted, + MakePlayerRecord(entity.DeletedBy), + NormalizeDatabaseTime(entity.DeletedAt), + entity.Seen); + } + + public async Task GetServerBanAsNoteAsync(int id) { await using var db = await GetDb(); @@ -1082,22 +1163,37 @@ public virtual async Task AddAdminMessage(AdminMessage message) return null; var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - return new ServerBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, player, - ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, ban.BanTime, - ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, ban.Hidden, - ban.Unban?.UnbanningAdmin == null + return new ServerBanNoteRecord( + ban.Id, + MakeRoundRecord(ban.Round), + MakePlayerRecord(player), + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy), + ban.BanTime, + MakePlayerRecord(ban.LastEditedBy), + ban.LastEditedAt, + ban.ExpirationTime, + ban.Hidden, + MakePlayerRecord(ban.Unban?.UnbanningAdmin == null ? null : await db.DbContext.Player.SingleOrDefaultAsync(p => - p.UserId == ban.Unban.UnbanningAdmin.Value), + p.UserId == ban.Unban.UnbanningAdmin.Value)), ban.Unban?.UnbanTime); } - public async Task GetServerRoleBanAsNoteAsync(int id) + public async Task GetServerRoleBanAsNoteAsync(int id) { await using var db = await GetDb(); var ban = await db.DbContext.RoleBan - .Include(b => b.Unban) + .Include(ban => ban.Unban) + .Include(ban => ban.Round) + .ThenInclude(r => r!.Server) + .Include(ban => ban.CreatedBy) + .Include(ban => ban.LastEditedBy) + .Include(ban => ban.Unban) .SingleOrDefaultAsync(b => b.Id == id); if (ban is null) @@ -1108,36 +1204,48 @@ public virtual async Task AddAdminMessage(AdminMessage message) ban.Unban is null ? null : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin); - return new ServerRoleBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, - player, ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, - ban.BanTime, ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, - ban.Hidden, new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, - unbanningAdmin, ban.Unban?.UnbanTime); + + return new ServerRoleBanNoteRecord( + ban.Id, + MakeRoundRecord(ban.Round), + MakePlayerRecord(player), + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy), + ban.BanTime, + MakePlayerRecord(ban.LastEditedBy), + ban.LastEditedAt, + ban.ExpirationTime, + ban.Hidden, + new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, + MakePlayerRecord(unbanningAdmin), + ban.Unban?.UnbanTime); } - public async Task> GetAllAdminRemarks(Guid player) + public async Task> GetAllAdminRemarks(Guid player) { await using var db = await GetDb(); - List notes = new(); + List notes = new(); notes.AddRange( - await (from note in db.DbContext.AdminNotes - where note.PlayerUserId == player && - !note.Deleted && - (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) - select note) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.LastEditedBy) - .Include(note => note.Player) - .ToListAsync()); + (await (from note in db.DbContext.AdminNotes + where note.PlayerUserId == player && + !note.Deleted && + (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) + select note) + .Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.LastEditedBy) + .Include(note => note.Player) + .ToListAsync()).Select(MakeAdminNoteRecord)); notes.AddRange(await GetActiveWatchlistsImpl(db, player)); notes.AddRange(await GetMessagesImpl(db, player)); notes.AddRange(await GetServerBansAsNotesForUser(db, player)); notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); return notes; } - public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); @@ -1146,39 +1254,39 @@ public async Task EditAdminNote(int id, string message, NoteSeverity severity, b note.Severity = severity; note.Secret = secret; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); var note = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync(); note.Message = message; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); var note = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync(); note.Message = message; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1186,12 +1294,12 @@ public async Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) note.Deleted = true; note.DeletedById = deletedBy; - note.DeletedAt = deletedAt; + note.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1199,12 +1307,12 @@ public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedA watchlist.Deleted = true; watchlist.DeletedById = deletedBy; - watchlist.DeletedAt = deletedAt; + watchlist.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1212,12 +1320,12 @@ public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) message.Deleted = true; message.DeletedById = deletedBy; - message.DeletedAt = deletedAt; + message.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1225,12 +1333,12 @@ public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime delete ban.Hidden = true; ban.LastEditedById = deletedBy; - ban.LastEditedAt = deletedAt; + ban.LastEditedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1238,40 +1346,40 @@ public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime de roleBan.Hidden = true; roleBan.LastEditedById = deletedBy; - roleBan.LastEditedAt = deletedAt; + roleBan.LastEditedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task> GetVisibleAdminRemarks(Guid player) + public async Task> GetVisibleAdminRemarks(Guid player) { await using var db = await GetDb(); - List notesCol = new(); + List notesCol = new(); notesCol.AddRange( - await (from note in db.DbContext.AdminNotes - where note.PlayerUserId == player && - !note.Secret && - !note.Deleted && - (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) - select note) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.Player) - .ToListAsync()); + (await (from note in db.DbContext.AdminNotes + where note.PlayerUserId == player && + !note.Secret && + !note.Deleted && + (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) + select note) + .Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.Player) + .ToListAsync()).Select(MakeAdminNoteRecord)); notesCol.AddRange(await GetMessagesImpl(db, player)); return notesCol; } - public async Task> GetActiveWatchlists(Guid player) + public async Task> GetActiveWatchlists(Guid player) { await using var db = await GetDb(); return await GetActiveWatchlistsImpl(db, player); } - protected async Task> GetActiveWatchlistsImpl(DbGuard db, Guid player) + protected async Task> GetActiveWatchlistsImpl(DbGuard db, Guid player) { - return await (from watchlist in db.DbContext.AdminWatchlists + var entities = await (from watchlist in db.DbContext.AdminWatchlists where watchlist.PlayerUserId == player && !watchlist.Deleted && (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime) @@ -1282,27 +1390,34 @@ protected async Task> GetActiveWatchlistsImpl(DbGuard db, G .Include(note => note.LastEditedBy) .Include(note => note.Player) .ToListAsync(); + + return entities.Select(MakeAdminWatchlistRecord).ToList(); } - public async Task> GetMessages(Guid player) + private AdminWatchlistRecord MakeAdminWatchlistRecord(AdminWatchlist entity) + { + return new AdminWatchlistRecord(entity.Id, MakeRoundRecord(entity.Round), MakePlayerRecord(entity.Player), entity.PlaytimeAtNote, entity.Message, MakePlayerRecord(entity.CreatedBy), NormalizeDatabaseTime(entity.CreatedAt), MakePlayerRecord(entity.LastEditedBy), NormalizeDatabaseTime(entity.LastEditedAt), NormalizeDatabaseTime(entity.ExpirationTime), entity.Deleted, MakePlayerRecord(entity.DeletedBy), NormalizeDatabaseTime(entity.DeletedAt)); + } + + public async Task> GetMessages(Guid player) { await using var db = await GetDb(); return await GetMessagesImpl(db, player); } - protected async Task> GetMessagesImpl(DbGuard db, Guid player) + protected async Task> GetMessagesImpl(DbGuard db, Guid player) { - return await (from message in db.DbContext.AdminMessages - where message.PlayerUserId == player && - !message.Deleted && - (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime) - select message) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.LastEditedBy) - .Include(note => note.Player) - .ToListAsync(); + var entities = await (from message in db.DbContext.AdminMessages + where message.PlayerUserId == player && !message.Deleted && + (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime) + select message).Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.LastEditedBy) + .Include(note => note.Player) + .ToListAsync(); + + return entities.Select(MakeAdminMessageRecord).ToList(); } public async Task MarkMessageAsSeen(int id) @@ -1314,7 +1429,7 @@ public async Task MarkMessageAsSeen(int id) } // These two are here because they get converted into notes later - protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) + protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) { // You can't group queries, as player will not always exist. When it doesn't, the // whole query returns nothing @@ -1329,17 +1444,27 @@ protected async Task> GetServerBansAsNotesForUser(DbGuard db .Include(ban => ban.Unban) .ToArrayAsync(); - var banNotes = new List(); + var banNotes = new List(); foreach (var ban in bans) { - var banNote = new ServerBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, player, - ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, ban.BanTime, - ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, ban.Hidden, - ban.Unban?.UnbanningAdmin == null + var banNote = new ServerBanNoteRecord( + ban.Id, + MakeRoundRecord(ban.Round), + MakePlayerRecord(player), + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy), + NormalizeDatabaseTime(ban.BanTime), + MakePlayerRecord(ban.LastEditedBy), + NormalizeDatabaseTime(ban.LastEditedAt), + NormalizeDatabaseTime(ban.ExpirationTime), + ban.Hidden, + MakePlayerRecord(ban.Unban?.UnbanningAdmin == null ? null : await db.DbContext.Player.SingleOrDefaultAsync( - p => p.UserId == ban.Unban.UnbanningAdmin.Value), - ban.Unban?.UnbanTime); + p => p.UserId == ban.Unban.UnbanningAdmin.Value)), + NormalizeDatabaseTime(ban.Unban?.UnbanTime)); banNotes.Add(banNote); } @@ -1347,7 +1472,7 @@ protected async Task> GetServerBansAsNotesForUser(DbGuard db return banNotes; } - protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) + protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) { // Server side query var bansQuery = await db.DbContext.RoleBan @@ -1366,7 +1491,7 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor .Select(banGroup => banGroup) .ToArray(); - List bans = new(); + List bans = new(); var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); foreach (var banGroup in bansEnumerable) { @@ -1376,11 +1501,22 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor if (firstBan.Unban?.UnbanningAdmin is not null) unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value); - bans.Add(new ServerRoleBanNote(firstBan.Id, firstBan.RoundId, firstBan.Round, firstBan.PlayerUserId, - player, firstBan.PlaytimeAtNote, firstBan.Reason, firstBan.Severity, firstBan.CreatedBy, - firstBan.BanTime, firstBan.LastEditedBy, firstBan.LastEditedAt, firstBan.ExpirationTime, - firstBan.Hidden, banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), - unbanningAdmin, firstBan.Unban?.UnbanTime)); + bans.Add(new ServerRoleBanNoteRecord( + firstBan.Id, + MakeRoundRecord(firstBan.Round), + MakePlayerRecord(player), + firstBan.PlaytimeAtNote, + firstBan.Reason, + firstBan.Severity, + MakePlayerRecord(firstBan.CreatedBy), + NormalizeDatabaseTime(firstBan.BanTime), + MakePlayerRecord(firstBan.LastEditedBy), + NormalizeDatabaseTime(firstBan.LastEditedAt), + NormalizeDatabaseTime(firstBan.ExpirationTime), + firstBan.Hidden, + banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), + MakePlayerRecord(unbanningAdmin), + NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); } return bans; @@ -1388,6 +1524,16 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor #endregion + // SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc. + // Normalize DateTimes here so they're always Utc. Thanks. + protected abstract DateTime NormalizeDatabaseTime(DateTime time); + + [return: NotNullIfNotNull(nameof(time))] + protected DateTime? NormalizeDatabaseTime(DateTime? time) + { + return time != null ? NormalizeDatabaseTime(time.Value) : time; + } + protected abstract Task GetDb([CallerMemberName] string? name = null); protected void LogDbOp(string? name) diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 7deeeb8e95a..5fda2a7e10b 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -92,9 +92,9 @@ public Task EditServerBan( int id, string reason, NoteSeverity severity, - DateTime? expiration, + DateTimeOffset? expiration, Guid editedBy, - DateTime editedAt); + DateTimeOffset editedAt); /// /// Update ban exemption information for a player. @@ -146,9 +146,9 @@ public Task EditServerRoleBan( int id, string reason, NoteSeverity severity, - DateTime? expiration, + DateTimeOffset? expiration, Guid editedBy, - DateTime editedAt); + DateTimeOffset editedAt); #endregion #region Playtime @@ -239,7 +239,7 @@ Task AddConnectionLogAsync( #region Uploaded Resources Logs - Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data); + Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data); Task PurgeUploadedResourceLogAsync(int days); @@ -247,33 +247,33 @@ Task AddConnectionLogAsync( #region Rules - Task GetLastReadRules(NetUserId player); - Task SetLastReadRules(NetUserId player, DateTime time); + Task GetLastReadRules(NetUserId player); + Task SetLastReadRules(NetUserId player, DateTimeOffset time); #endregion #region Admin Notes - Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task GetAdminNote(int id); - Task GetAdminWatchlist(int id); - Task GetAdminMessage(int id); - Task GetServerBanAsNoteAsync(int id); - Task GetServerRoleBanAsNoteAsync(int id); - Task> GetAllAdminRemarks(Guid player); - Task> GetVisibleAdminNotes(Guid player); - Task> GetActiveWatchlists(Guid player); - Task> GetMessages(Guid player); - Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt); - Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt); - Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt); - Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt); - Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt); + Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task GetAdminNote(int id); + Task GetAdminWatchlist(int id); + Task GetAdminMessage(int id); + Task GetServerBanAsNoteAsync(int id); + Task GetServerRoleBanAsNoteAsync(int id); + Task> GetAllAdminRemarks(Guid player); + Task> GetVisibleAdminNotes(Guid player); + Task> GetActiveWatchlists(Guid player); + Task> GetMessages(Guid player); + Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); Task MarkMessageAsSeen(int id); #endregion @@ -423,7 +423,7 @@ public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); } - public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); @@ -470,7 +470,7 @@ public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban)); } - public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt)); @@ -665,7 +665,7 @@ public Task RemoveFromWhitelistAsync(NetUserId player) return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player)); } - public Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data) + public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.AddUploadedResourceLogAsync(user, date, path, data)); @@ -677,19 +677,19 @@ public Task PurgeUploadedResourceLogAsync(int days) return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days)); } - public Task GetLastReadRules(NetUserId player) + public Task GetLastReadRules(NetUserId player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetLastReadRules(player)); } - public Task SetLastReadRules(NetUserId player, DateTime time) + public Task SetLastReadRules(NetUserId player, DateTimeOffset time) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.SetLastReadRules(player, time)); } - public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminNote @@ -702,15 +702,15 @@ public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote Message = message, Severity = severity, Secret = secret, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminNote(note)); } - public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminWatchlist @@ -721,15 +721,15 @@ public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeA PlayerUserId = player, PlaytimeAtNote = playtimeAtNote, Message = message, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminWatchlist(note)); } - public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminMessage @@ -740,108 +740,108 @@ public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtN PlayerUserId = player, PlaytimeAtNote = playtimeAtNote, Message = message, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminMessage(note)); } - public Task GetAdminNote(int id) + public Task GetAdminNote(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminNote(id)); } - public Task GetAdminWatchlist(int id) + public Task GetAdminWatchlist(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminWatchlist(id)); } - public Task GetAdminMessage(int id) + public Task GetAdminMessage(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminMessage(id)); } - public Task GetServerBanAsNoteAsync(int id) + public Task GetServerBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); } - public Task GetServerRoleBanAsNoteAsync(int id) + public Task GetServerRoleBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id)); } - public Task> GetAllAdminRemarks(Guid player) + public Task> GetAllAdminRemarks(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAllAdminRemarks(player)); } - public Task> GetVisibleAdminNotes(Guid player) + public Task> GetVisibleAdminNotes(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetVisibleAdminRemarks(player)); } - public Task> GetActiveWatchlists(Guid player) + public Task> GetActiveWatchlists(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetActiveWatchlists(player)); } - public Task> GetMessages(Guid player) + public Task> GetMessages(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetMessages(player)); } - public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminNote(id, message, severity, secret, editedBy, editedAt, expiryTime)); } - public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, expiryTime)); } - public Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminMessage(id, message, editedBy, editedAt, expiryTime)); } - public Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminNote(id, deletedBy, deletedAt)); } - public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminWatchlist(id, deletedBy, deletedAt)); } - public Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); } - public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); } - public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt)); diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index 8a8f26e503b..c81e735868a 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -162,7 +162,7 @@ private static IQueryable MakeBanLookupQuery( if (!includeUnbanned) { query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now)); + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } if (exemptFlags is { } exempt) @@ -354,7 +354,7 @@ private static IQueryable MakeRoleBanLookupQuery( if (!includeUnbanned) { query = query?.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now)); + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } query = query!.Distinct(); @@ -457,17 +457,6 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRole } #endregion - protected override PlayerRecord MakePlayerRecord(Player record) - { - return new PlayerRecord( - new NetUserId(record.UserId), - new DateTimeOffset(record.FirstSeenTime), - record.LastSeenUserName, - new DateTimeOffset(record.LastSeenTime), - record.LastSeenAddress, - record.LastSeenHWId?.ToImmutableArray()); - } - public override async Task AddConnectionLogAsync( NetUserId userId, string userName, @@ -532,6 +521,12 @@ WHERE to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('engl return db.AdminLog; } + protected override DateTime NormalizeDatabaseTime(DateTime time) + { + DebugTools.Assert(time.Kind == DateTimeKind.Utc); + return time; + } + private async Task GetDbImpl([CallerMemberName] string? name = null) { LogDbOp(name); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 90bbec023a8..46886fe4d18 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Utility; namespace Content.Server.Database { @@ -350,17 +351,6 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba } #endregion - protected override PlayerRecord MakePlayerRecord(Player record) - { - return new PlayerRecord( - new NetUserId(record.UserId), - new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), - record.LastSeenUserName, - new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), - record.LastSeenAddress, - record.LastSeenHWId?.ToImmutableArray()); - } - private static ServerBanDef? ConvertBan(ServerBan? ban) { if (ban == null) @@ -546,6 +536,12 @@ public override async Task AddAdminMessage(AdminMessage message) return await base.AddAdminMessage(message); } + protected override DateTime NormalizeDatabaseTime(DateTime time) + { + DebugTools.Assert(time.Kind == DateTimeKind.Unspecified); + return DateTime.SpecifyKind(time, DateTimeKind.Utc); + } + private async Task GetDbImpl([CallerMemberName] string? name = null) { LogDbOp(name); diff --git a/Content.Server/Database/ServerRoleBanNote.cs b/Content.Server/Database/ServerRoleBanNote.cs deleted file mode 100644 index 6db8110db85..00000000000 --- a/Content.Server/Database/ServerRoleBanNote.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Content.Shared.Database; - -namespace Content.Server.Database -{ - public record ServerRoleBanNote(int Id, int? RoundId, Round? Round, Guid? PlayerUserId, Player? Player, - TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, Player? CreatedBy, DateTime CreatedAt, - Player? LastEditedBy, DateTime? LastEditedAt, DateTime? ExpirationTime, bool Deleted, string[] Roles, - Player? UnbanningAdmin, DateTime? UnbanTime) : IAdminRemarksCommon; -} diff --git a/Content.Server/GameTicking/GameTicker.Replays.cs b/Content.Server/GameTicking/GameTicker.Replays.cs index f23482585cc..e09e07b8673 100644 --- a/Content.Server/GameTicking/GameTicker.Replays.cs +++ b/Content.Server/GameTicking/GameTicker.Replays.cs @@ -1,4 +1,4 @@ -using Content.Shared.CCVar; +using Content.Shared.CCVar; using Robust.Shared; using Robust.Shared.ContentPack; using Robust.Shared.Replays; @@ -48,6 +48,10 @@ private void ReplayStartRound() var tempDir = _cfg.GetCVar(CCVars.ReplayAutoRecordTempDir); ResPath? moveToPath = null; + // Set the round end player and text back to null to prevent it from writing the previous round's data. + _replayRoundPlayerInfo = null; + _replayRoundText = null; + if (!string.IsNullOrEmpty(tempDir)) { var baseReplayPath = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath(); @@ -124,9 +128,7 @@ private void ReplaysOnRecordingStopped(MappingDataNode metadata) metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo); metadata["roundEndText"] = new ValueDataNode(_replayRoundText); metadata["server_id"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.ServerId)); - // These should be set to null to prepare them for the next round. - _replayRoundPlayerInfo = null; - _replayRoundText = null; + metadata["roundId"] = new ValueDataNode(RoundId.ToString()); } private ResPath GetAutoReplayPath() diff --git a/Content.Shared/Actions/ActionContainerSystem.cs b/Content.Shared/Actions/ActionContainerSystem.cs index 17bcf11bff1..1c5a3ba0d93 100644 --- a/Content.Shared/Actions/ActionContainerSystem.cs +++ b/Content.Shared/Actions/ActionContainerSystem.cs @@ -37,7 +37,7 @@ public override void Initialize() private void OnMindAdded(EntityUid uid, ActionsContainerComponent component, MindAddedMessage args) { - if(!_mind.TryGetMind(uid, out var mindId, out _)) + if (!_mind.TryGetMind(uid, out var mindId, out _)) return; if (!TryComp(mindId, out var mindActionContainerComp)) return; @@ -143,20 +143,15 @@ public void TransferAction( return; DebugTools.AssertEqual(action.Container, newContainer); - DebugTools.AssertNull(action.AttachedEntity); - - if (attached != null) - _actions.AddActionDirect(attached.Value, actionId, action: action); - DebugTools.AssertEqual(action.AttachedEntity, attached); } /// /// Transfers all actions from one container to another, while keeping the attached entity the same. /// - /// <remarks> + /// /// While the attached entity should be the same at the end, this will actually remove and then re-grant the action. - /// </remarks> + /// public void TransferAllActions( EntityUid from, EntityUid to, @@ -305,11 +300,11 @@ private void OnEntityInserted(EntityUid uid, ActionsContainerComponent component if (!_actions.TryGetActionData(args.Entity, out var data)) return; - DebugTools.Assert(data.AttachedEntity == null || data.Container != EntityUid.Invalid); - DebugTools.Assert(data.Container == null || data.Container == uid); - - data.Container = uid; - Dirty(uid, component); + if (data.Container != uid) + { + data.Container = uid; + Dirty(args.Entity, data); + } var ev = new ActionAddedEvent(args.Entity, data); RaiseLocalEvent(uid, ref ev); @@ -320,21 +315,17 @@ private void OnEntityRemoved(EntityUid uid, ActionsContainerComponent component, if (args.Container.ID != ActionsContainerComponent.ContainerId) return; - // Actions should only be getting removed while terminating or moving outside of PVS range. - DebugTools.Assert(Terminating(args.Entity) - || _netMan.IsServer // I love gibbing code - || _timing.ApplyingState); - if (!_actions.TryGetActionData(args.Entity, out var data, false)) return; - // No event - the only entity that should care about this is the entity that the action was provided to. - if (data.AttachedEntity != null) - _actions.RemoveAction(data.AttachedEntity.Value, args.Entity, null, data); - var ev = new ActionRemovedEvent(args.Entity, data); RaiseLocalEvent(uid, ref ev); + + if (data.Container == null) + return; + data.Container = null; + Dirty(args.Entity, data); } private void OnActionAdded(EntityUid uid, ActionsContainerComponent component, ActionAddedEvent args) diff --git a/Content.Shared/Actions/ActionsComponent.cs b/Content.Shared/Actions/ActionsComponent.cs index b810e98d4d3..a081a238671 100644 --- a/Content.Shared/Actions/ActionsComponent.cs +++ b/Content.Shared/Actions/ActionsComponent.cs @@ -26,8 +26,6 @@ public ActionsComponentState(HashSet actions) } } -public readonly record struct ActionMetaData(bool ClientExclusive); - /// /// Determines how the action icon appears in the hotbar for item actions. /// diff --git a/Content.Shared/Actions/BaseActionComponent.cs b/Content.Shared/Actions/BaseActionComponent.cs index 291d9a3ea29..cce7b912c76 100644 --- a/Content.Shared/Actions/BaseActionComponent.cs +++ b/Content.Shared/Actions/BaseActionComponent.cs @@ -4,7 +4,8 @@ namespace Content.Shared.Actions; -// TODO this should be an IncludeDataFields of each action component type, not use inheritance +// TODO ACTIONS make this a seprate component and remove the inheritance stuff. +// TODO ACTIONS convert to auto comp state? // TODO add access attribute. Need to figure out what to do with decal & mapping actions. // [Access(typeof(SharedActionsSystem))] @@ -72,9 +73,9 @@ public abstract partial class BaseActionComponent : Component [DataField("charges")] public int? Charges; /// - /// The max charges this action has, set automatically from + /// The max charges this action has. If null, this is set automatically from on mapinit. /// - public int MaxCharges; + [DataField] public int? MaxCharges; /// /// If enabled, charges will regenerate after a is complete @@ -130,7 +131,7 @@ public EntityUid? EntityIcon /// /// What entity, if any, currently has this action in the actions component? /// - [ViewVariables] public EntityUid? AttachedEntity; + [DataField] public EntityUid? AttachedEntity; /// /// If true, this will cause the the action event to always be raised directed at the action performer/user instead of the action's container/provider. @@ -171,7 +172,7 @@ public abstract class BaseActionComponentState : ComponentState public (TimeSpan Start, TimeSpan End)? Cooldown; public TimeSpan? UseDelay; public int? Charges; - public int MaxCharges; + public int? MaxCharges; public bool RenewCharges; public NetEntity? Container; public NetEntity? EntityIcon; diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 90508bff9d7..a6c40c7ae35 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -8,7 +8,6 @@ using Content.Shared.Interaction; using Content.Shared.Inventory.Events; using Content.Shared.Mind; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -35,9 +34,13 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnActionMapInit); + SubscribeLocalEvent(OnActionMapInit); + SubscribeLocalEvent(OnActionMapInit); + + SubscribeLocalEvent(OnActionShutdown); + SubscribeLocalEvent(OnActionShutdown); + SubscribeLocalEvent(OnActionShutdown); SubscribeLocalEvent(OnDidEquip); SubscribeLocalEvent(OnHandEquipped); @@ -60,10 +63,19 @@ public override void Initialize() SubscribeAllEvent(OnActionRequest); } - private void OnInit(EntityUid uid, BaseActionComponent component, MapInitEvent args) + private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args) + { + if (component.Charges == null) + return; + + component.MaxCharges ??= component.Charges.Value; + Dirty(uid, component); + } + + private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args) { - if (component.Charges != null) - component.MaxCharges = component.Charges.Value; + if (component.AttachedEntity != null && !TerminatingOrDeleted(component.AttachedEntity.Value)) + RemoveAction(component.AttachedEntity.Value, uid, action: component); } private void OnShutdown(EntityUid uid, ActionsComponent component, ComponentShutdown args) diff --git a/Content.Shared/Administration/Notes/SharedAdminNote.cs b/Content.Shared/Administration/Notes/SharedAdminNote.cs index e209d3721e9..09d4f3f9478 100644 --- a/Content.Shared/Administration/Notes/SharedAdminNote.cs +++ b/Content.Shared/Administration/Notes/SharedAdminNote.cs @@ -1,4 +1,5 @@ using Content.Shared.Database; +using Robust.Shared.Network; using Robust.Shared.Serialization; namespace Content.Shared.Administration.Notes; @@ -6,7 +7,7 @@ namespace Content.Shared.Administration.Notes; [Serializable, NetSerializable] public sealed record SharedAdminNote( int Id, // Id of note, message, watchlist, ban or role ban. Should be paired with NoteType to uniquely identify a shared admin note. - Guid Player, // Notes player + NetUserId Player, // Notes player int? Round, // Which round was it added in? string? ServerName, // Which server was this added on? TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note diff --git a/Content.Shared/Administration/PlayerInfo.cs b/Content.Shared/Administration/PlayerInfo.cs index 74fd7e9dc06..93f1aa0b393 100644 --- a/Content.Shared/Administration/PlayerInfo.cs +++ b/Content.Shared/Administration/PlayerInfo.cs @@ -4,7 +4,7 @@ namespace Content.Shared.Administration { [Serializable, NetSerializable] - public record PlayerInfo( + public sealed record PlayerInfo( string Username, string CharacterName, string IdentityName, @@ -20,5 +20,15 @@ public record PlayerInfo( public string PlaytimeString => _playtimeString ??= OverallPlaytime?.ToString("%d':'hh':'mm") ?? Loc.GetString("generic-unknown-title"); + + public bool Equals(PlayerInfo? other) + { + return other?.SessionId == SessionId; + } + + public override int GetHashCode() + { + return SessionId.GetHashCode(); + } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 32e3ae6e1d4..b719bab1df1 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -794,7 +794,7 @@ public static readonly CVarDef /// Default severity for server bans /// public static readonly CVarDef ServerBanDefaultSeverity = - CVarDef.Create("admin.server_ban_default_severity", "high", CVar.ARCHIVE | CVar.SERVER); + CVarDef.Create("admin.server_ban_default_severity", "High", CVar.ARCHIVE | CVar.SERVER); /// /// Minimum explosion intensity to create an admin alert message. -1 to disable the alert. diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index 32ecdd4ba6f..6e762aa5984 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -212,8 +212,6 @@ protected void UpdateAppearance(Entity container, Entity sol if (solution.GetPrimaryReagentId() is { } reagent) AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearanceComponent); - else - AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, string.Empty, appearanceComponent); } /// diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index f71afa7f479..5d6d9d21208 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -89,6 +89,15 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype [DataField] public SpriteSpecifier? MetamorphicSprite { get; private set; } = null; + [DataField] + public int MetamorphicMaxFillLevels { get; private set; } = 0; + + [DataField] + public string? MetamorphicFillBaseName { get; private set; } = null; + + [DataField] + public bool MetamorphicChangeColor { get; private set; } = true; + /// /// If this reagent is part of a puddle is it slippery. /// @@ -102,7 +111,7 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype [DataField] public float Viscosity; - /// + /// /// Should this reagent work on the dead? /// [DataField] @@ -163,7 +172,7 @@ public void ReactionPlant(EntityUid? plantHolder, ReagentQuantity amount, Soluti if (plantMetabolizable.ShouldLog) { var entity = args.SolutionEntity; - EntitySystem.Get().Add(LogType.ReagentEffect, plantMetabolizable.LogImpact, + entMan.System().Add(LogType.ReagentEffect, plantMetabolizable.LogImpact, $"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID:reagent} applied on entity {entMan.ToPrettyString(entity):entity} at {entMan.GetComponent(entity).Coordinates:coordinates}"); } diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index f03745006fa..0138de7a98f 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -170,12 +170,7 @@ private void OnRemoveToggleable(EntityUid uid, ToggleableClothingComponent compo // "outside" of the container or not. This means that if a hardsuit takes too much damage, the helmet will also // automatically be deleted. - // remove action. - if (_actionsSystem.TryGetActionData(component.ActionEntity, out var action) && - action.AttachedEntity != null) - { - _actionsSystem.RemoveAction(action.AttachedEntity.Value, component.ActionEntity); - } + _actionsSystem.RemoveAction(component.ActionEntity); if (component.ClothingUid != null && !_netMan.IsClient) QueueDel(component.ClothingUid.Value); @@ -199,13 +194,7 @@ private void OnRemoveAttached(EntityUid uid, AttachedClothingComponent component if (toggleComp.LifeStage > ComponentLifeStage.Running) return; - // remove action. - if (_actionsSystem.TryGetActionData(toggleComp.ActionEntity, out var action) && - action.AttachedEntity != null) - { - _actionsSystem.RemoveAction(action.AttachedEntity.Value, toggleComp.ActionEntity); - } - + _actionsSystem.RemoveAction(toggleComp.ActionEntity); RemComp(component.AttachedUid, toggleComp); } diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index af4516df7de..6826d1e6eaa 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -181,7 +181,17 @@ public static HumanoidCharacterProfile RandomWithSpecies(string species = Shared age = random.Next(speciesPrototype.MinAge, speciesPrototype.OldAge); // people don't look and keep making 119 year old characters with zero rp, cap it at middle aged } - var gender = sex == Sex.Male ? Gender.Male : Gender.Female; + var gender = Gender.Epicene; + + switch (sex) + { + case Sex.Male: + gender = Gender.Male; + break; + case Sex.Female: + gender = Gender.Female; + break; + } var name = GetName(species, gender); @@ -297,16 +307,16 @@ public HumanoidCharacterProfile WithAntagPreferences(IEnumerable antagPr public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref) { var list = new List(_antagPreferences); - if(pref) + if (pref) { - if(!list.Contains(antagId)) + if (!list.Contains(antagId)) { list.Add(antagId); } } else { - if(list.Contains(antagId)) + if (list.Contains(antagId)) { list.Remove(antagId); } @@ -319,16 +329,16 @@ public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) var list = new List(_traitPreferences); // TODO: Maybe just refactor this to HashSet? Same with _antagPreferences - if(pref) + if (pref) { - if(!list.Contains(traitId)) + if (!list.Contains(traitId)) { list.Add(traitId); } } else { - if(list.Contains(traitId)) + if (list.Contains(traitId)) { list.Remove(traitId); } diff --git a/Resources/Audio/Ambience/ambitrain1.ogg b/Resources/Audio/Ambience/ambitrain1.ogg new file mode 100644 index 00000000000..bc244259221 Binary files /dev/null and b/Resources/Audio/Ambience/ambitrain1.ogg differ diff --git a/Resources/Audio/Ambience/ambitrain2.ogg b/Resources/Audio/Ambience/ambitrain2.ogg new file mode 100644 index 00000000000..3a3222a75f8 Binary files /dev/null and b/Resources/Audio/Ambience/ambitrain2.ogg differ diff --git a/Resources/Audio/Ambience/ambitrain3.ogg b/Resources/Audio/Ambience/ambitrain3.ogg new file mode 100644 index 00000000000..3f18b482342 Binary files /dev/null and b/Resources/Audio/Ambience/ambitrain3.ogg differ diff --git a/Resources/Audio/Ambience/attributions.yml b/Resources/Audio/Ambience/attributions.yml index 5ea0c248188..3e6e039cd79 100644 --- a/Resources/Audio/Ambience/attributions.yml +++ b/Resources/Audio/Ambience/attributions.yml @@ -97,8 +97,22 @@ copyright: "Taken from /vg/station" source: "https://github.com/vgstation-coders/vgstation13/commit/23303188abe6fe31b114a218a1950d7325a23730" +- files: ["ambitrain1.ogg"] + license: "CC-BY-4.0" + copyright: "Created by Badgie42 on Freesound.com, converted to Mono by TheShuEd" + source: "https://freesound.org/people/Badgie42/sounds/399876/" + +- files: ["ambitrain2.ogg"] + license: "CC0-1.0" + copyright: "Created by Hallkom on Freesound.com, cropped and converted to Mono by TheShuEd" + source: "https://freesound.org/people/Hallkom/sounds/588486/" + +- files: ["ambitrain3.ogg"] + license: "CC0-1.0" + copyright: "Created by ldezem on Freesound.com, cropped and converted to Mono by TheShuEd" + source: "https://freesound.org/people/ldezem/sounds/528238/" - files: ["anomaly_scary.ogg"] license: "CC0-1.0" - copyright: "Created by dimbark1, edited and converted to mono by TheShuEd (github)" - source: "https://freesound.org/people/dimbark1/sounds/316797/" \ No newline at end of file + copyright: "Created by dimbark1, edited and converted to mono by TheShuEd" + source: "https://freesound.org/people/dimbark1/sounds/316797/" diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 3e82a672e9e..dc16879d1c7 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,118 +1,4 @@ Entries: -- author: Ubaser - changes: - - message: You can now purchase kobolds from cargo as an alternative to monkeys! - type: Add - id: 5464 - time: '2023-12-28T05:32:55.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22108 -- author: TheShuEd - changes: - - message: Tesla has been added to the game! Engineers can build a tesla power generator - using a tesla generator, tesla coils, and grounding wands that can be purchased - from the cargo. - type: Add - id: 5465 - time: '2023-12-28T13:11:50.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/21124 -- author: TheShuEd - changes: - - message: thieves can now appear in rounds with nuclear operatives - type: Add - id: 5466 - time: '2023-12-28T23:50:25.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23102 -- author: EmoGarbage404 - changes: - - message: Added station beacons. These devices create custom labels on the station - map. Order them from cargo today. - type: Add - id: 5467 - time: '2023-12-29T00:02:21.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23136 -- author: lzk228 - changes: - - message: Directional barricade is constructable now! - type: Tweak - id: 5468 - time: '2023-12-29T00:45:10.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22988 -- author: themias - changes: - - message: Toy swords can be turned on. - type: Fix - id: 5469 - time: '2023-12-29T00:59:23.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23137 -- author: Ubaser - changes: - - message: Magboots produced by science now have new sprites. - type: Add - id: 5470 - time: '2023-12-29T03:08:08.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23107 -- author: metalgearsloth - changes: - - message: Added sounds to UI clicking and hovering. - type: Add - id: 5471 - time: '2023-12-29T04:43:37.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22410 -- author: FairlySadPanda - changes: - - message: Talking, whispering and emoting will now strip out any markup tags in - the input text. This also means intercoms tuned to the science channel will - no longer squawk out "[bold]", and so on, when reporting on research unlocks. - type: Fix - id: 5472 - time: '2023-12-30T00:38:11.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23055 -- author: Admiral-Obvious-001 - changes: - - message: Normalized researched secfab shotgun ammo damages to align with the rest - of the ammo sizes. - type: Tweak - id: 5473 - time: '2023-12-30T05:34:27.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23163 -- author: metalgearsloth - changes: - - message: Fix decals such as grass drawing over shuttles on planet maps. - type: Fix - id: 5474 - time: '2023-12-30T16:12:39.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22953 -- author: metalgearsloth - changes: - - message: Fix mobs not gibbing upon shuttle FTL. - type: Fix - id: 5475 - time: '2023-12-30T16:12:52.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22952 -- author: metalgearsloth - changes: - - message: Surveillance cameras now cycle their animation forwards and backwards. - type: Add - id: 5476 - time: '2023-12-30T16:14:14.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/22950 -- author: EmoGarbage404 - changes: - - message: Added the Hyper convection protolathe and autolathe to Industrial Engineering - tech. These lathes have a discounted printing price at the cost of venting hot - air into the surroundings. - type: Add - - message: Added the industrial ore processor, an improved version with discounted - printing prices, to salvage equipment technology. - type: Add - - message: Moved autolathe, protolathe, circuit imprinter, ore processor, and material - reclaimer boards to be available by default in the circuit imprinter. - type: Tweak - - message: Removed machine part upgrades from all lathes. - type: Remove - id: 5477 - time: '2023-12-30T16:18:58.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/23202 - author: Ubaser changes: - message: The uncloneable trait has been removed. @@ -3863,3 +3749,104 @@ id: 5963 time: '2024-02-17T23:55:58.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/25357 +- author: genderGeometries + changes: + - message: Allicin, nutriment, and vitamin can now be centrifuged. Protein and fat + can now be electrolyzed. + type: Add + id: 5964 + time: '2024-02-19T06:05:43.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25366 +- author: azurerosegarden + changes: + - message: Drinks in the Solar's Best Hot Drinks menu now show their contents + type: Fix + id: 5965 + time: '2024-02-19T15:40:43.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25301 +- author: Lank + changes: + - message: Nymphs can now open doors and chirp. + type: Tweak + id: 5966 + time: '2024-02-19T17:11:20.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25363 +- author: PotentiallyTom + changes: + - message: Gave guidebooks to the 4 learner roles + type: Tweak + id: 5967 + time: '2024-02-19T18:54:02.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25388 +- author: jamessimo + changes: + - message: vending machine UI improved + type: Tweak + id: 5968 + time: '2024-02-19T22:18:27.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25377 +- author: marbow + changes: + - message: Moths can now eat plushies and with a distinctive sound! + type: Add + id: 5969 + time: '2024-02-19T22:35:35.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25382 +- author: ArchPigeon + changes: + - message: Liquid Tritium can now be used as a chem in flamethrowers + type: Add + id: 5970 + time: '2024-02-19T22:37:45.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25281 +- author: ElectroJr + changes: + - message: Fix actions sometimes disappearing. + type: Fix + id: 5971 + time: '2024-02-20T02:08:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25395 +- author: PJB3005 + changes: + - message: Fixed timezone issues with the admin notes window. + type: Fix + id: 5972 + time: '2024-02-20T09:13:31.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25280 +- author: PJB3005 + changes: + - message: Fixed selection behavior for player lists such as the ban panel or ahelp + window. + type: Fix + id: 5973 + time: '2024-02-20T09:13:48.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25248 +- author: Tonydatguy + changes: + - message: Ore Crabs will now take structural damage. + type: Tweak + id: 5974 + time: '2024-02-20T10:43:16.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25390 +- author: Golinth + changes: + - message: The mindshield outline now flashes! + type: Tweak + id: 5975 + time: '2024-02-20T22:26:48.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25409 +- author: landwhale + changes: + - message: Resprited Nettles & Death Nettles. + type: Tweak + id: 5976 + time: '2024-02-20T23:58:08.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25421 +- author: takemysoult + changes: + - message: Explosive Technology changed to tier 2 arsenal and the cost has increased + to 10 000 + type: Tweak + id: 5977 + time: '2024-02-21T00:56:39.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25397 diff --git a/Resources/Locale/en-US/accent/accents.ftl b/Resources/Locale/en-US/accent/accents.ftl index 7b72e5705b9..301c589449d 100644 --- a/Resources/Locale/en-US/accent/accents.ftl +++ b/Resources/Locale/en-US/accent/accents.ftl @@ -118,3 +118,9 @@ accent-words-kobold-6 = Gronk! accent-words-kobold-7 = Hiss! accent-words-kobold-8 = Eeee! accent-words-kobold-9 = Yip. + +# Nymph +accent-words-nymph-1 = Chirp! +accent-words-nymph-2 = Churr... +accent-words-nymph-3 = Cheep? +accent-words-nymph-4 = Chrrup! diff --git a/Resources/Locale/en-US/tiles/tiles.ftl b/Resources/Locale/en-US/tiles/tiles.ftl index 42e0c082235..478175fc674 100644 --- a/Resources/Locale/en-US/tiles/tiles.ftl +++ b/Resources/Locale/en-US/tiles/tiles.ftl @@ -1,6 +1,7 @@ tiles-space = space tiles-plating = plating tiles-lattice = lattice +tiles-lattice-train = train lattice tiles-steel-floor = steel tile tiles-steel-floor-mini = steel mini-tile tiles-steel-floor-pavement = steel pavement diff --git a/Resources/Locale/en-US/vending-machines/vending-machine.ftl b/Resources/Locale/en-US/vending-machines/vending-machine.ftl index d527f42cfb6..637a9c7568e 100644 --- a/Resources/Locale/en-US/vending-machines/vending-machine.ftl +++ b/Resources/Locale/en-US/vending-machines/vending-machine.ftl @@ -1 +1,3 @@ vending-machine-thanks = Thanks for using { $name }! +vending-machine-flavor-left = Request refills at cargo +vending-machine-flavor-right = v1.1 diff --git a/Resources/Prototypes/Accents/full_replacements.yml b/Resources/Prototypes/Accents/full_replacements.yml index 07413b83b97..1febb376328 100644 --- a/Resources/Prototypes/Accents/full_replacements.yml +++ b/Resources/Prototypes/Accents/full_replacements.yml @@ -166,3 +166,11 @@ - accent-words-kobold-7 - accent-words-kobold-8 - accent-words-kobold-9 + +- type: accent + id: nymph + fullReplacements: + - accent-words-nymph-1 + - accent-words-nymph-2 + - accent-words-nymph-3 + - accent-words-nymph-4 diff --git a/Resources/Prototypes/Body/Organs/diona.yml b/Resources/Prototypes/Body/Organs/diona.yml index 23df396dd1e..8b4b78cac06 100644 --- a/Resources/Prototypes/Body/Organs/diona.yml +++ b/Resources/Prototypes/Body/Organs/diona.yml @@ -170,7 +170,7 @@ - type: entity id: OrganDionaNymphStomach - parent: MobDionaNymph + parent: MobDionaNymphAccent noSpawn: true name: diona nymph suffix: Stomach @@ -182,7 +182,7 @@ - type: entity id: OrganDionaNymphLungs - parent: MobDionaNymph + parent: MobDionaNymphAccent noSpawn: true name: diona nymph suffix: Lungs diff --git a/Resources/Prototypes/Damage/containers.yml b/Resources/Prototypes/Damage/containers.yml index 328c7a5d793..fb40e9b658f 100644 --- a/Resources/Prototypes/Damage/containers.yml +++ b/Resources/Prototypes/Damage/containers.yml @@ -51,4 +51,4 @@ - type: damageContainer id: ShadowHaze supportedTypes: - - Heat \ No newline at end of file + - Heat diff --git a/Resources/Prototypes/DeltaV/Voice/speech_emotes.yml b/Resources/Prototypes/DeltaV/Voice/speech_emotes.yml index 42d25fdbf51..66210294f2a 100644 --- a/Resources/Prototypes/DeltaV/Voice/speech_emotes.yml +++ b/Resources/Prototypes/DeltaV/Voice/speech_emotes.yml @@ -85,17 +85,6 @@ - cawing - cawed -- type: emote - id: Chirp - category: Vocal - chatMessages: [chirps.] - chatTriggers: - - chirps. - - chirp. - - chirp! - - chirping. - - chirped. - #Vulpkanin - type: emote id: Bark diff --git a/Resources/Prototypes/Entities/Effects/portal.yml b/Resources/Prototypes/Entities/Effects/portal.yml index 19b6fc1be31..eb69ac821f5 100644 --- a/Resources/Prototypes/Entities/Effects/portal.yml +++ b/Resources/Prototypes/Entities/Effects/portal.yml @@ -69,6 +69,38 @@ - type: Portal canTeleportToOtherMaps: true +- type: entity + id: PortalGatewayBlue + parent: BasePortal + components: + - type: Sprite + noRot: true + sprite: Structures/Machines/gateway.rsi + color: SkyBlue + layers: + - state: portal + - type: PointLight + color: SkyBlue + radius: 3 + energy: 1 + netsync: false + +- type: entity + id: PortalGatewayOrange + parent: BasePortal + components: + - type: Sprite + noRot: true + sprite: Structures/Machines/gateway.rsi + color: OrangeRed + layers: + - state: portal + - type: PointLight + color: OrangeRed + radius: 3 + energy: 1 + netsync: false + - type: entity id: ShadowPortal name: shadow rift @@ -90,4 +122,4 @@ range: 6 volume: -3 sound: - path: /Audio/Ambience/anomaly_scary.ogg \ No newline at end of file + path: /Audio/Ambience/anomaly_scary.ogg diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index b55ec81c665..7af568b40c9 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -3180,11 +3180,25 @@ attributes: gender: epicene - type: Speech + speechVerb: Plant - type: Tag tags: + - DoorBumpOpener - VimPilot + - type: Emoting + - type: BodyEmotes + soundsId: Nymph - type: Reform actionPrototype: DionaReformAction reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed + +- type: entity + parent: MobDionaNymph + id: MobDionaNymphAccent # No talky. For non-brain & wild nymphs + suffix: Accent + components: + - type: ReplacementAccent + accent: nymph + \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 9694148287d..3631510fff7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -99,6 +99,8 @@ - type: NpcFactionMember factions: - SimpleHostile + - type: Damageable + damageContainer: StructuralInorganic - type: entity parent: MobOreCrab diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml index a23acbe932b..699d1aa9ab1 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml @@ -184,6 +184,9 @@ reagents: - ReagentId: HotCocoa Quantity: 20 + - type: Icon + sprite: Objects/Consumable/Drinks/hot_coco.rsi + state: icon-vend - type: Sprite sprite: Objects/Consumable/Drinks/hot_coco.rsi layers: @@ -209,6 +212,9 @@ reagents: - ReagentId: Coffee Quantity: 20 + - type: Icon + sprite: Objects/Consumable/Drinks/hot_coffee.rsi + state: icon-vend - type: Sprite sprite: Objects/Consumable/Drinks/hot_coffee.rsi layers: @@ -252,6 +258,9 @@ reagents: - ReagentId: Tea Quantity: 20 + - type: Icon + sprite: Objects/Consumable/Drinks/teacup.rsi + state: icon-vend-tea - type: Sprite sprite: Objects/Consumable/Drinks/teacup.rsi layers: @@ -277,6 +286,9 @@ reagents: - ReagentId: GreenTea Quantity: 20 + - type: Icon + sprite: Objects/Consumable/Drinks/teacup.rsi + state: icon-vend-green-tea - type: Sprite sprite: Objects/Consumable/Drinks/teacup.rsi layers: diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 5560a49d437..497b9987265 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -10,6 +10,7 @@ - type: Tag tags: - Payload + - ClothMade - type: EmitSoundOnUse sound: collection: ToySqueak @@ -38,6 +39,18 @@ Cloth: 100 - type: StaticPrice price: 5 + - type: Food + requiresSpecialDigestion: true + useSound: + collection: ToySqueak + delay: 2 + - type: SolutionContainerManager + solutions: + food: + maxVol: 10 + reagents: + - ReagentId: Fiber + Quantity: 10 - type: entity parent: BasePlushie @@ -114,8 +127,15 @@ solutions: bee: reagents: - - ReagentId: GroundBee - Quantity: 10 + - ReagentId: GroundBee + Quantity: 10 + food: + maxVol: 10 + reagents: + - ReagentId: GroundBee + Quantity: 5 + - ReagentId: Fiber + Quantity: 5 - type: Clothing quickEquip: false sprite: Objects/Fun/toys.rsi @@ -143,6 +163,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/mousesqueek.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/mousesqueek.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -212,6 +236,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Voice/Arachnid/arachnid_laugh.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Voice/Arachnid/arachnid_laugh.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -237,6 +265,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/weh.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/weh.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -271,6 +303,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/muffled_weh.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/muffled_weh.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -304,10 +340,20 @@ wideAnimationRotation: 180 soundHit: path: /Audio/Items/Toys/toy_rustle.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/toy_rustle.ogg - type: SolutionContainerManager solutions: plushie: maxVol: 1 + food: + maxVol: 10 + reagents: + - ReagentId: Fiber + Quantity: 10 + - type: RefillableSolution solution: plushie - type: SolutionContainerVisuals @@ -433,6 +479,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Effects/bite.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Effects/bite.ogg - type: MeleeWeapon wideAnimationRotation: -90 soundHit: @@ -466,6 +516,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/rattle.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/rattle.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -485,6 +539,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/mousesqueek.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/mousesqueek.ogg - type: MeleeWeapon wideAnimationRotation: -90 soundHit: @@ -518,6 +576,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Items/Toys/quack.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/mousesqueek.ogg - type: entity parent: BasePlushie @@ -536,6 +598,10 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Voice/Vox/shriek1.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Voice/Vox/shriek1.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: @@ -572,11 +638,83 @@ - type: EmitSoundOnTrigger sound: path: /Audio/Weapons/Xeno/alien_spitacid.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/mousesqueek.ogg - type: MeleeWeapon wideAnimationRotation: 180 soundHit: path: /Audio/Weapons/Xeno/alien_spitacid.ogg +- type: entity + parent: BasePlushie + id: PlushiePenguin + name: penguin plushie + description: I use arch btw! + components: + - type: Sprite + state: plushie_penguin + +- type: entity + parent: BasePlushie + id: PlushieHuman + name: human plushie + description: This is a felt plush of a human. All craftsmanship is of the lowest quality. The human is naked. The human is crying. The human is screaming. + components: + - type: Sprite + state: plushie_human + - type: EmitSoundOnUse + sound: + path: /Audio/Voice/Human/malescream_1.ogg + - type: EmitSoundOnLand + sound: + path: /Audio/Voice/Human/malescream_2.ogg + - type: EmitSoundOnActivate + sound: + path: /Audio/Voice/Human/malescream_3.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Voice/Human/malescream_1.ogg + - type: MeleeWeapon + soundHit: + path: /Audio/Voice/Human/malescream_4.ogg + - type: EmitSoundOnTrigger + sound: + path: /Audio/Voice/Human/malescream_5.ogg + +- type: entity + parent: BasePlushie + id: PlushieMoth + name: moth plushie + description: Cute and fluffy moth plushie. Enjoy, bz! + components: + - type: Sprite + state: plushie_moth + - type: EmitSoundOnUse + sound: + path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak + - type: EmitSoundOnLand + sound: + path: /Audio/Voice/Moth/moth_scream.ogg # DeltaV - Make the mothplushie scream on landing + - type: EmitSoundOnActivate + sound: + path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak + - type: EmitSoundOnTrigger + sound: + path: /Audio/Voice/Moth/moth_scream.ogg # DeltaV - Make the mothplushie scream on trigger + - type: MeleeWeapon + soundHit: + path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak + - type: Construction # DeltaV - Mothroach hide craft, see Prototypes/Nyanotrasen/Recipes/Crafting/Graphs/fun/mothplushie.yml + graph: MothPlushie + node: plush + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Voice/Moth/moth_chitter.ogg + ## Cheapo Figurines - type: entity @@ -657,6 +795,10 @@ - type: MeleeWeapon soundHit: path: /Audio/Items/Toys/ian.ogg + - type: Food + requiresSpecialDigestion: true + useSound: + path: /Audio/Items/Toys/ian.ogg ## Toyweapons @@ -1235,71 +1377,11 @@ drink: maxVol: 100 reagents: - - ReagentId: SpaceGlue - Quantity: 100 + - ReagentId: SpaceGlue + Quantity: 100 - type: TrashOnSolutionEmpty solution: drink -- type: entity - parent: BasePlushie - id: PlushieMoth - name: moth plushie - description: Cute and fluffy moth plushie. Enjoy, bz! - components: - - type: Sprite - state: plushie_moth - - type: EmitSoundOnUse - sound: - path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak - - type: EmitSoundOnLand - sound: - path: /Audio/Voice/Moth/moth_scream.ogg # DeltaV - Make the mothplushie scream on landing - - type: EmitSoundOnActivate - sound: - path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak - - type: EmitSoundOnTrigger - sound: - path: /Audio/Voice/Moth/moth_scream.ogg # DeltaV - Make the mothplushie scream on trigger - - type: MeleeWeapon - soundHit: - path: /Audio/Voice/Moth/moth_squeak.ogg # DeltaV - Give back the mothplushie the squeak - - type: Construction # DeltaV - Mothroach hide craft, see Prototypes/Nyanotrasen/Recipes/Crafting/Graphs/fun/mothplushie.yml - graph: MothPlushie - node: plush - -- type: entity - parent: BasePlushie - id: PlushiePenguin - name: penguin plushie - description: I use arch btw! - components: - - type: Sprite - state: plushie_penguin - -- type: entity - parent: BasePlushie - id: PlushieHuman - name: human plushie - description: This is a felt plush of a human. All craftsmanship is of the lowest quality. The human is naked. The human is crying. The human is screaming. - components: - - type: Sprite - state: plushie_human - - type: EmitSoundOnUse - sound: - path: /Audio/Voice/Human/malescream_1.ogg - - type: EmitSoundOnLand - sound: - path: /Audio/Voice/Human/malescream_2.ogg - - type: EmitSoundOnActivate - sound: - path: /Audio/Voice/Human/malescream_3.ogg - - type: MeleeWeapon - soundHit: - path: /Audio/Voice/Human/malescream_4.ogg - - type: EmitSoundOnTrigger - sound: - path: /Audio/Voice/Human/malescream_5.ogg - - type: entity parent: BaseItem id: NewtonCradle diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml index 18d6f19bb11..3a40d355a4f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml @@ -345,10 +345,16 @@ # Pills - type: entity - name: dexalin pill (10u) + name: pill (dexalin 10u) parent: Pill id: PillDexalin components: + - type: Pill + pillType: 15 + - type: Sprite + state: pill16 + - type: Label + currentLabel: dexalin 10u - type: SolutionContainerManager solutions: food: @@ -359,20 +365,29 @@ - type: entity + name: pill canister (dexalin 10u) parent: PillCanister id: PillCanisterDexalin suffix: Dexalin, 7 components: + - type: Label + currentLabel: dexalin 10u - type: StorageFill contents: - id: PillDexalin amount: 7 - type: entity - name: dylovene pill (10u) + name: pill (dylovene 10u) parent: Pill id: PillDylovene components: + - type: Pill + pillType: 9 + - type: Sprite + state: pill10 + - type: Label + currentLabel: dylovene 10u - type: SolutionContainerManager solutions: food: @@ -382,20 +397,29 @@ Quantity: 10 - type: entity + name: pill canister (dylovene 10u) parent: PillCanister id: PillCanisterDylovene suffix: Dylovene, 5 components: + - type: Label + currentLabel: dylovene 10u - type: StorageFill contents: - id: PillDylovene amount: 5 - type: entity - name: hyronalin pill (10u) + name: pill (hyronalin 10u) parent: Pill id: PillHyronalin components: + - type: Pill + pillType: 16 + - type: Sprite + state: pill17 + - type: Label + currentLabel: hyronalin 10u - type: SolutionContainerManager solutions: food: @@ -405,20 +429,29 @@ Quantity: 10 - type: entity + name: pill canister (hyronalin 10u) parent: PillCanister id: PillCanisterHyronalin suffix: Hyronalin, 5 components: + - type: Label + currentLabel: hyronalin 10u - type: StorageFill contents: - id: PillHyronalin amount: 5 - type: entity - name: iron pill (10u) + name: pill (iron 10u) parent: Pill id: PillIron components: + - type: Pill + pillType: 13 + - type: Sprite + state: pill14 + - type: Label + currentLabel: iron 10u - type: SolutionContainerManager solutions: food: @@ -428,10 +461,16 @@ Quantity: 10 - type: entity - name: copper pill (10u) + name: pill (copper 10u) parent: Pill id: PillCopper components: + - type: Pill + pillType: 12 + - type: Sprite + state: pill13 + - type: Label + currentLabel: copper 10u - type: SolutionContainerManager solutions: food: @@ -441,30 +480,42 @@ Quantity: 10 - type: entity + name: pill canister (iron 10u) parent: PillCanister id: PillCanisterIron suffix: Iron, 5 components: + - type: Label + currentLabel: iron 10u - type: StorageFill contents: - id: PillIron amount: 5 - type: entity + name: pill canister (copper 10u) parent: PillCanister id: PillCanisterCopper suffix: Copper, 5 components: + - type: Label + currentLabel: copper 10u - type: StorageFill contents: - id: PillCopper amount: 5 - type: entity - name: kelotane pill (10u) + name: pill (kelotane 10u) parent: Pill id: PillKelotane components: + - type: Pill + pillType: 3 + - type: Sprite + state: pill4 + - type: Label + currentLabel: kelotane 10u - type: SolutionContainerManager solutions: food: @@ -474,20 +525,29 @@ Quantity: 10 - type: entity + name: pill canister (kelotane 10u) parent: PillCanister id: PillCanisterKelotane suffix: Kelotane, 5 components: + - type: Label + currentLabel: kelotane 10u - type: StorageFill contents: - id: PillKelotane amount: 5 - type: entity - name: dermaline pill (10u) + name: pill (dermaline 10u) parent: Pill id: PillDermaline components: + - type: Pill + pillType: 7 + - type: Sprite + state: pill8 + - type: Label + currentLabel: dermaline 10u - type: SolutionContainerManager solutions: food: @@ -497,10 +557,13 @@ Quantity: 10 - type: entity + name: pill canister (dermaline 10u) parent: PillCanister id: PillCanisterDermaline suffix: Dermaline, 5 components: + - type: Label + currentLabel: dermaline 10u - type: StorageFill contents: - id: PillDermaline @@ -520,10 +583,16 @@ Quantity: 15 - type: entity - name: tricordrazine pill (10u) + name: pill (tricordrazine 10u) parent: Pill id: PillTricordrazine components: + - type: Pill + pillType: 2 + - type: Sprite + state: pill3 + - type: Label + currentLabel: tricordrazine 10u - type: SolutionContainerManager solutions: food: @@ -533,20 +602,29 @@ Quantity: 10 - type: entity + name: pill canister (tricordrazine 10u) parent: PillCanister id: PillCanisterTricordrazine suffix: Tricordrazine, 5 components: + - type: Label + currentLabel: tricordrazine 10u - type: StorageFill contents: - id: PillTricordrazine amount: 5 - type: entity - name: bicaridine pill (10u) + name: pill (bicaridine 10u) parent: Pill id: PillBicaridine components: + - type: Pill + pillType: 4 + - type: Sprite + state: pill5 + - type: Label + currentLabel: bicaridine 10u - type: SolutionContainerManager solutions: food: @@ -556,23 +634,29 @@ Quantity: 10 - type: entity + name: pill canister (bicaridine 10u) parent: PillCanister id: PillCanisterBicaridine suffix: Bicaridine, 5 components: + - type: Label + currentLabel: bicaridine 10u - type: StorageFill contents: - id: PillBicaridine amount: 5 - type: entity - name: charcoal pill (10u) + name: pill (charcoal 10u) parent: Pill id: PillCharcoal components: + - type: Pill + pillType: 20 - type: Sprite - sprite: Objects/Specific/Chemistry/pills.rsi state: pill21 + - type: Label + currentLabel: charcoal 10u - type: SolutionContainerManager solutions: food: @@ -582,10 +666,13 @@ Quantity: 10 - type: entity + name: pill canister (charcoal 10u) parent: PillCanister id: PillCanisterCharcoal suffix: Charcoal, 3 components: + - type: Label + currentLabel: charcoal 10u - type: StorageFill contents: - id: PillCharcoal diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml index 56fe96973d0..943fdad5180 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml @@ -319,7 +319,7 @@ # medical modules - type: entity - id: BorgModuleDiagnosis + id: BorgModuleDiagnosis # todo: reuse when med refractor is finished parent: [ BaseBorgModuleMedical, BaseProviderBorgModule ] name: diagnosis cyborg module components: @@ -374,6 +374,7 @@ - state: icon-chemist - type: ItemBorgModule items: + - HandheldHealthAnalyzerUnpowered - Beaker - Beaker - BorgDropper diff --git a/Resources/Prototypes/Entities/Structures/Decoration/statues.yml b/Resources/Prototypes/Entities/Structures/Decoration/statues.yml index c8806c5d957..1781190e29d 100644 --- a/Resources/Prototypes/Entities/Structures/Decoration/statues.yml +++ b/Resources/Prototypes/Entities/Structures/Decoration/statues.yml @@ -6,6 +6,7 @@ description: An ancient marble statue. The subject is depicted with a floor-length braid and is wielding a red toolbox. components: - type: Sprite + noRot: true sprite: Structures/Decoration/statues.rsi state: venus_red drawdepth: Mobs @@ -19,6 +20,7 @@ description: An ancient marble statue. The subject is depicted with a floor-length braid and is wielding a blue toolbox. components: - type: Sprite + noRot: true sprite: Structures/Decoration/statues.rsi state: venus_blue drawdepth: Mobs @@ -31,6 +33,7 @@ description: A bananium statue. It portrays the return of the savior who will rise up and lead the clowns to the great honk. components: - type: Sprite + noRot: true sprite: Structures/Decoration/statues.rsi state: bananium_clown drawdepth: Mobs diff --git a/Resources/Prototypes/Entities/Structures/Furniture/bench.yml b/Resources/Prototypes/Entities/Structures/Furniture/bench.yml index 76bebd61666..c61f103dda0 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/bench.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/bench.yml @@ -49,3 +49,18 @@ - type: Construction graph: Seat node: redComfBench + +- type: entity + id: BenchBlueComfy + suffix: Solo. Blue + parent: BenchComfy + components: + - type: Sprite + layers: + - state: bench_solo_base + color: "#767e82" + - state: bench_solo_cover + color: "#334e6d" + - type: Construction + graph: Seat + node: blueComfBench diff --git a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml index 2e381bf9bdd..07a25d7b643 100644 --- a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml @@ -148,6 +148,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 15 + energy: 1 + softness: 0.9 + color: "#EEEEFF" - type: entity parent: AlwaysPoweredWallLight @@ -197,6 +202,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 10 + energy: 2.5 + softness: 0.9 + color: "#FFAF38" - type: entity parent: AlwaysPoweredWallLight @@ -369,6 +379,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#47f8ff" - type: entity id: AlwaysPoweredlightCyan @@ -391,6 +406,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#39a1ff" - type: entity id: AlwaysPoweredlightBlue @@ -413,6 +433,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#ff66cc" - type: entity id: AlwaysPoweredlightPink @@ -435,6 +460,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#ff8227" - type: entity id: AlwaysPoweredlightOrange @@ -457,6 +487,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#fb4747" - type: entity id: AlwaysPoweredlightRed @@ -479,6 +514,11 @@ damage: types: Heat: 5 + - type: PointLight + radius: 8 + energy: 3 + softness: 0.5 + color: "#52ff39" - type: entity id: AlwaysPoweredlightGreen diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 0486f38d451..ed476b4c10f 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -85,6 +85,8 @@ runningState: building staticRecipes: - Wirecutter + - Igniter + - Signaller - Screwdriver - Welder - Wrench diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml index faae8b004b7..2ca6b51e5c2 100644 --- a/Resources/Prototypes/Entities/Structures/Windows/window.yml +++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml @@ -236,4 +236,4 @@ sprite: Structures/Windows/cracks_diagonal.rsi - type: Construction graph: WindowDiagonal - node: windowDiagonal + node: windowDiagonal \ No newline at end of file diff --git a/Resources/Prototypes/Maps/Pools/default.yml b/Resources/Prototypes/Maps/Pools/default.yml index ac2bf25f615..7de404f7e4e 100644 --- a/Resources/Prototypes/Maps/Pools/default.yml +++ b/Resources/Prototypes/Maps/Pools/default.yml @@ -1,20 +1,6 @@ - type: gameMapPool id: DefaultMapPool maps: - #- Aspid - #- Bagel - #- Barratry - #- Box - #- Cluster - #- Core - #- Fland - #- Kettle - #- Marathon - #- Meta - #- Omega - #- Origin - #- Saltern - #- Packed # DeltaV - Arena - Asterisk diff --git a/Resources/Prototypes/Parallaxes/train.yml b/Resources/Prototypes/Parallaxes/train.yml new file mode 100644 index 00000000000..e506043c852 --- /dev/null +++ b/Resources/Prototypes/Parallaxes/train.yml @@ -0,0 +1,68 @@ +- type: parallax + id: TrainStation + layers: + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Parallaxes/AspidParallaxBG.png" + slowness: 0.998046875 + scale: "0.5, 0.5" + scrolling: "0, -0.098046875" + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars" + configPath: "/Prototypes/Parallaxes/parallax_config_stars.toml" + slowness: 0.996625 + scrolling: "0, -0.196625" + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars_dim" + configPath: "/Prototypes/Parallaxes/parallax_config_stars_dim.toml" + slowness: 0.989375 + scrolling: "0, -0.209375" + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars_faster" + configPath: "/Prototypes/Parallaxes/parallax_config_stars-2.toml" + slowness: 0.987265625 + scrolling: "0, -0.287265625" + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars_dim_faster" + configPath: "/Prototypes/Parallaxes/parallax_config_stars_dim-2.toml" + slowness: 0.984352 + scrolling: "0, -0.384352" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Parallaxes/AspidParallaxBG.png" + slowness: 0.978046875 + scrolling: "0, -0.578046875" + scale: "1, 1" + tiled: false + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Parallaxes/Asteroids.png" + slowness: 0.968046875 + scrolling: "0, -0.568046875" + scale: "1.3, 1.3" + worldHomePosition: "-624, 333" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Parallaxes/AspidParallaxNeb.png" + slowness: 0.969046875 + scrolling: "0, -0.569046875" + scale: "0.5, 0.5" + worldHomePosition: "0, 0" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Parallaxes/Asteroids.png" + slowness: 0.938046875 + scrolling: "0, -0.708046875" + scale: "1, 1" + layersLQ: + - texture: + !type:GeneratedParallaxTextureSource + id: "" + configPath: "/Prototypes/Parallaxes/parallax_config.toml" + slowness: 0.875 + scrolling: "0, -0.475" + layersLQUseHQ: false diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 77a387da7b5..ce93bbc4631 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -67,7 +67,7 @@ desc: reagent-desc-green-tea physicalDesc: reagent-physical-desc-aromatic flavor: tea - color: "#C33F00" + color: "#7EB626" metamorphicSprite: sprite: Objects/Consumable/Drinks/glass_green.rsi state: icon @@ -107,7 +107,7 @@ desc: reagent-desc-iced-green-tea physicalDesc: reagent-physical-desc-aromatic flavor: icedtea - color: "#CE4200" + color: "#5B821B" metamorphicSprite: sprite: Objects/Consumable/Drinks/glass_green.rsi state: icon @@ -119,7 +119,7 @@ desc: reagent-desc-iced-tea physicalDesc: reagent-physical-desc-aromatic flavor: icedtea - color: "#104038" + color: "#6C3916" metamorphicSprite: sprite: Objects/Consumable/Drinks/icedteaglass.rsi state: icon diff --git a/Resources/Prototypes/Reagents/gases.yml b/Resources/Prototypes/Reagents/gases.yml index 54bdbf3a405..2566076be7f 100644 --- a/Resources/Prototypes/Reagents/gases.yml +++ b/Resources/Prototypes/Reagents/gases.yml @@ -87,6 +87,12 @@ tileReactions: - !type:FlammableTileReaction temperatureMultiplier: 2.0 + reactiveEffects: + Flammable: + methods: [ Touch ] + effects: + - !type:FlammableReaction + multiplier: 0.8 metabolisms: Poison: effects: diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/furniture/seats.yml b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/seats.yml index 9089de34e81..53d4ba70085 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/furniture/seats.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/seats.yml @@ -70,6 +70,13 @@ doAfter: 1 - material: Cloth amount: 1 + - to: blueComfBench + steps: + - material: Steel + amount: 2 + doAfter: 1 + - material: Cloth + amount: 1 - node: chair entity: Chair @@ -219,3 +226,20 @@ doAfter: 1 - tool: Screwing doAfter: 1 + + - node: blueComfBench + entity: BenchBlueComfy + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetSteel1 + amount: 2 + - !type:SpawnPrototype + prototype: MaterialCloth1 + amount: 1 + steps: + - tool: Cutting + doAfter: 1 + - tool: Screwing + doAfter: 1 diff --git a/Resources/Prototypes/Recipes/Construction/furniture.yml b/Resources/Prototypes/Recipes/Construction/furniture.yml index efe85a8546a..19558c461b9 100644 --- a/Resources/Prototypes/Recipes/Construction/furniture.yml +++ b/Resources/Prototypes/Recipes/Construction/furniture.yml @@ -220,6 +220,23 @@ conditions: - !type:TileNotBlocked +- type: construction + name: comfortable blue bench + id: BlueComfBench + graph: Seat + startNode: start + targetNode: blueComfBench + category: construction-category-furniture + description: A bench with an extremely comfortable backrest. + icon: + sprite: Structures/Furniture/Bench/comf_bench.rsi + state: full + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked + #tables - type: construction name: steel table diff --git a/Resources/Prototypes/Recipes/Lathes/clothing.yml b/Resources/Prototypes/Recipes/Lathes/clothing.yml index 56f09e5c68a..69f5226fc2d 100644 --- a/Resources/Prototypes/Recipes/Lathes/clothing.yml +++ b/Resources/Prototypes/Recipes/Lathes/clothing.yml @@ -678,7 +678,7 @@ result: ClothingNeckMantleCap completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -686,7 +686,7 @@ result: ClothingNeckMantleCE completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -694,7 +694,7 @@ result: ClothingNeckMantleCMO completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -702,7 +702,7 @@ result: ClothingNeckMantleHOP completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -710,7 +710,7 @@ result: ClothingNeckMantleHOS completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -718,7 +718,7 @@ result: ClothingNeckMantleRD completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 - type: latheRecipe @@ -726,7 +726,7 @@ result: ClothingNeckMantleQM completetime: 2.8 materials: - Cloth: 150 + Cloth: 200 Durathread: 150 @@ -1086,74 +1086,74 @@ result: ClothingNeckTieRed completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckTieDet result: ClothingNeckTieDet completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckTieSci result: ClothingNeckTieSci completetime: 2 materials: - Cloth: 50 + Cloth: 200 # Scarfs - type: latheRecipe id: ClothingNeckScarfStripedGreen result: ClothingNeckScarfStripedGreen completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedBlue result: ClothingNeckScarfStripedBlue completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedRed result: ClothingNeckScarfStripedRed completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedBrown result: ClothingNeckScarfStripedBrown completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedLightBlue result: ClothingNeckScarfStripedLightBlue completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedOrange result: ClothingNeckScarfStripedOrange completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedBlack result: ClothingNeckScarfStripedBlack completetime: 2 materials: - Cloth: 50 + Cloth: 200 - type: latheRecipe id: ClothingNeckScarfStripedPurple result: ClothingNeckScarfStripedPurple completetime: 2 materials: - Cloth: 50 + Cloth: 200 diff --git a/Resources/Prototypes/Recipes/Reactions/food.yml b/Resources/Prototypes/Recipes/Reactions/food.yml index f758babde5e..50800395698 100644 --- a/Resources/Prototypes/Recipes/Reactions/food.yml +++ b/Resources/Prototypes/Recipes/Reactions/food.yml @@ -334,3 +334,84 @@ # Oxygen: 1 # Hydrogen: 2 +- type: reaction + id: AllicinBreakdown + source: true + requiredMixerCategories: + - Centrifuge + reactants: + Allicin: + amount: 4 + products: + Sulfur: 2 + Carbon: 1 + Water: 1 + +- type: reaction + id: NutrimentBreakdown + source: true + requiredMixerCategories: + - Centrifuge + reactants: + Nutriment: + amount: 5 + products: + Water: 2 + Carbon: 2 + Sugar: 1 + +- type: reaction + id: FatBreakdown + source: true + requiredMixerCategories: + - Electrolysis + reactants: + Fat: + amount: 15 + products: + Carbon: 5 + Hydrogen: 10 + Oxygen: 1 + +# Proteins are hydrocarbons +- type: reaction + id: UncookedAnimalProteinBreakdown + source: true + requiredMixerCategories: + - Electrolysis + reactants: + UncookedAnimalProteins: + amount: 10 + products: + Carbon: 2 + Hydrogen: 4 + Phosphorus: 3 + Oxygen: 1 + +- type: reaction + id: ProteinBreakdown + source: true + requiredMixerCategories: + - Electrolysis + reactants: + Protein: + amount: 10 + products: + Carbon: 2 + Hydrogen: 4 + Phosphorus: 3 + Oxygen: 1 + +# Vitamin = Healthy = Green = Nitrogenous +- type: reaction + id: VitaminBreakdown + source: true + requiredMixerCategories: + - Centrifuge + reactants: + Vitamin: + amount: 4 + products: + Carbon: 1 + Water: 1 + Nitrogen: 2 diff --git a/Resources/Prototypes/Research/arsenal.yml b/Resources/Prototypes/Research/arsenal.yml index 6946f5fe1d1..c288367b159 100644 --- a/Resources/Prototypes/Research/arsenal.yml +++ b/Resources/Prototypes/Research/arsenal.yml @@ -34,26 +34,6 @@ - MagazineBoxLightRifleIncendiary - MagazineBoxRifleIncendiary -- type: technology - id: ExplosiveTechnology - name: research-technology-explosive-technology - icon: - sprite: Objects/Devices/payload.rsi - state: payload-explosive-armed - discipline: Arsenal - tier: 1 - cost: 7500 - recipeUnlocks: - - Signaller - - SignallerAdvanced - - SignalTrigger - - VoiceTrigger - - TimerTrigger - - FlashPayload - - ExplosivePayload - - ChemicalPayload - - Igniter - - type: technology id: WeaponizedLaserManipulation name: research-technology-weaponized-laser-manipulation @@ -108,8 +88,42 @@ - MagazineBoxLightRifleUranium - MagazineBoxRifleUranium +- type: technology + id: AdvancedRiotControl + name: research-technology-advanced-riot-control + icon: + sprite: Objects/Weapons/Melee/truncheon.rsi + state: icon + discipline: Arsenal + tier: 1 + cost: 8000 + recipeUnlocks: + - ClothingEyesGlassesSecurity + - Truncheon + - TelescopicShield + - HoloprojectorSecurity + - WeaponDisablerSMG + # Tier 2 +- type: technology + id: ExplosiveTechnology + name: research-technology-explosive-technology + icon: + sprite: Objects/Devices/payload.rsi + state: payload-explosive-armed + discipline: Arsenal + tier: 2 + cost: 10000 + recipeUnlocks: + - SignallerAdvanced + - SignalTrigger + - VoiceTrigger + - TimerTrigger + - FlashPayload + - ExplosivePayload + - ChemicalPayload + - type: technology id: ConcentratedLaserWeaponry name: research-technology-concentrated-laser-weaponry @@ -134,22 +148,6 @@ recipeUnlocks: - WeaponXrayCannon -- type: technology - id: AdvancedRiotControl - name: research-technology-advanced-riot-control - icon: - sprite: Objects/Weapons/Melee/truncheon.rsi - state: icon - discipline: Arsenal - tier: 2 - cost: 8000 - recipeUnlocks: - - ClothingEyesGlassesSecurity - - Truncheon - - TelescopicShield - - HoloprojectorSecurity - - WeaponDisablerSMG - - type: technology id: BasicShuttleArmament name: research-technology-basic-shuttle-armament diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index 0ba629c8c5c..c3f83e0fc59 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -135,10 +135,9 @@ state: medical discipline: CivilianServices tier: 2 - cost: 7500 + cost: 5000 recipeUnlocks: - BorgModuleAdvancedTreatment - - BorgModuleDiagnosis - BorgModuleDefibrillator - type: technology diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml index 9f9889fdaea..e0b5a268ca2 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml @@ -29,6 +29,7 @@ id: TechnicalAssistantPDA belt: ClothingBeltUtilityEngineering ears: ClothingHeadsetEngineering + pocket2: BookEngineersHandbook innerClothingSkirt: ClothingUniformJumpskirtColorYellow satchel: ClothingBackpackSatchelEngineeringFilled duffelbag: ClothingBackpackDuffelEngineeringFilled diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml index 468d9632a0e..0166a3dfadb 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml @@ -25,6 +25,7 @@ id: MedicalInternPDA ears: ClothingHeadsetMedical belt: ClothingBeltMedicalFilled + pocket2: BookMedicalReferenceBook # innerClothingSkirt: ClothingUniformJumpskirtColorWhite # DeltaV satchel: ClothingBackpackSatchelMedicalFilled duffelbag: ClothingBackpackDuffelMedicalFilled diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml index bff76fd7bd5..4d4038d7c02 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml @@ -24,6 +24,7 @@ shoes: ClothingShoesColorWhite id: ResearchAssistantPDA ears: ClothingHeadsetScience + pocket2: BookScientistsGuidebook innerClothingSkirt: ClothingUniformJumpskirtColorWhite satchel: ClothingBackpackSatchelScienceFilled duffelbag: ClothingBackpackDuffelScienceFilled diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml index 7e7cd67c4a8..93b6a166e0b 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml @@ -32,6 +32,7 @@ id: SecurityCadetPDA ears: ClothingHeadsetSecurity # pocket1: WeaponPistolMk58Nonlethal # DeltaV - Security Cadet doesn't spawn with a gun + pocket2: BookSecurity innerClothingSkirt: ClothingUniformJumpskirtColorRed satchel: ClothingBackpackSatchelSecurityFilled duffelbag: ClothingBackpackDuffelSecurityFilled diff --git a/Resources/Prototypes/StatusEffects/health.yml b/Resources/Prototypes/StatusEffects/health.yml index 2ab90e75824..0a1df020c05 100644 --- a/Resources/Prototypes/StatusEffects/health.yml +++ b/Resources/Prototypes/StatusEffects/health.yml @@ -2,6 +2,6 @@ id: HealthIconFine priority: 1 icon: - sprite: Interface/Misc/health_icons.rsi + sprite: /Textures/Interface/Misc/health_icons.rsi state: Fine locationPreference: Right \ No newline at end of file diff --git a/Resources/Prototypes/StatusEffects/hunger.yml b/Resources/Prototypes/StatusEffects/hunger.yml index 294a7659b18..64366657132 100644 --- a/Resources/Prototypes/StatusEffects/hunger.yml +++ b/Resources/Prototypes/StatusEffects/hunger.yml @@ -3,7 +3,7 @@ id: HungerIconOverfed priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: overfed locationPreference: Right @@ -11,7 +11,7 @@ id: HungerIconPeckish priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: peckish locationPreference: Right @@ -19,7 +19,7 @@ id: HungerIconStarving priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: starving locationPreference: Right @@ -28,7 +28,7 @@ id: ThirstIconOverhydrated priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: overhydrated locationPreference: Left @@ -36,7 +36,7 @@ id: ThirstIconThirsty priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: thirsty locationPreference: Left @@ -44,6 +44,6 @@ id: ThirstIconParched priority: 5 icon: - sprite: Interface/Misc/food_icons.rsi + sprite: /Textures/Interface/Misc/food_icons.rsi state: parched locationPreference: Left diff --git a/Resources/Prototypes/StatusEffects/job.yml b/Resources/Prototypes/StatusEffects/job.yml index 7072cc44346..782cacfdd52 100644 --- a/Resources/Prototypes/StatusEffects/job.yml +++ b/Resources/Prototypes/StatusEffects/job.yml @@ -8,369 +8,369 @@ parent: JobIcon id: JobIconDetective icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Detective - type: statusIcon parent: JobIcon id: JobIconQuarterMaster icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: QuarterMaster - type: statusIcon parent: JobIcon id: JobIconBorg icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Borg - type: statusIcon parent: JobIcon id: JobIconBotanist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Botanist - type: statusIcon parent: JobIcon id: JobIconBoxer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Boxer - type: statusIcon parent: JobIcon id: JobIconAtmosphericTechnician icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: AtmosphericTechnician - type: statusIcon parent: JobIcon id: JobIconNanotrasen icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Nanotrasen - type: statusIcon parent: JobIcon id: JobIconPrisoner icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Prisoner - type: statusIcon parent: JobIcon id: JobIconJanitor icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Janitor - type: statusIcon parent: JobIcon id: JobIconChemist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Chemist - type: statusIcon parent: JobIcon id: JobIconStationEngineer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: StationEngineer - type: statusIcon parent: JobIcon id: JobIconSecurityOfficer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SecurityOfficer - type: statusIcon parent: JobIcon id: JobIconNoId icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: NoId - type: statusIcon parent: JobIcon id: JobIconChiefMedicalOfficer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ChiefMedicalOfficer - type: statusIcon parent: JobIcon id: JobIconRoboticist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Roboticist - type: statusIcon parent: JobIcon id: JobIconChaplain icon: - sprite: DeltaV/Interface/Misc/job_icons.rsi # DeltaV - Move Chaplain into Epistemics + sprite: /Textures/DeltaV/Interface/Misc/job_icons.rsi # DeltaV - Move Chaplain into Epistemics state: Chaplain # DeltaV - Move Chaplain into Epistemics - type: statusIcon parent: JobIcon id: JobIconLawyer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Lawyer - type: statusIcon parent: JobIcon id: JobIconUnknown icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Unknown - type: statusIcon parent: JobIcon id: JobIconLibrarian icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Librarian - type: statusIcon parent: JobIcon id: JobIconCargoTechnician icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: CargoTechnician - type: statusIcon parent: JobIcon id: JobIconScientist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Scientist - type: statusIcon parent: JobIcon id: JobIconResearchAssistant icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ResearchAssistant - type: statusIcon parent: JobIcon id: JobIconGeneticist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Geneticist - type: statusIcon parent: JobIcon id: JobIconClown icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Clown - type: statusIcon parent: JobIcon id: JobIconCaptain icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Captain - type: statusIcon parent: JobIcon id: JobIconHeadOfPersonnel icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadOfPersonnel - type: statusIcon parent: JobIcon id: JobIconVirologist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Virologist - type: statusIcon parent: JobIcon id: JobIconShaftMiner icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ShaftMiner - type: statusIcon parent: JobIcon id: JobIconPassenger icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Passenger - type: statusIcon parent: JobIcon id: JobIconChiefEngineer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ChiefEngineer - type: statusIcon parent: JobIcon id: JobIconBartender icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Bartender - type: statusIcon parent: JobIcon id: JobIconHeadOfSecurity icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadOfSecurity - type: statusIcon parent: JobIcon id: JobIconBrigmedic icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Brigmedic - type: statusIcon parent: JobIcon id: JobIconMedicalDoctor icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: MedicalDoctor - type: statusIcon parent: JobIcon id: JobIconParamedic icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Paramedic - type: statusIcon parent: JobIcon id: JobIconChef icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Chef - type: statusIcon parent: JobIcon id: JobIconWarden icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Warden - type: statusIcon parent: JobIcon id: JobIconResearchDirector icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ResearchDirector - type: statusIcon parent: JobIcon id: JobIconMime icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Mime - type: statusIcon parent: JobIcon id: JobIconMusician icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Musician - type: statusIcon parent: JobIcon id: JobIconReporter icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Reporter - type: statusIcon parent: JobIcon id: JobIconPsychologist icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Psychologist - type: statusIcon parent: JobIcon id: JobIconMedicalIntern icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: MedicalIntern - type: statusIcon parent: JobIcon id: JobIconTechnicalAssistant icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: TechnicalAssistant - type: statusIcon parent: JobIcon id: JobIconServiceWorker icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: ServiceWorker - type: statusIcon parent: JobIcon id: JobIconSecurityCadet icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SecurityCadet - type: statusIcon parent: JobIcon id: JobIconZombie # This is a perfectly legitimate profession to pursue icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Zombie - type: statusIcon parent: JobIcon id: JobIconZookeeper icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Zookeeper - type: statusIcon parent: JobIcon id: JobIconSeniorPhysician icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorPhysician - type: statusIcon parent: JobIcon id: JobIconSeniorOfficer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorOfficer - type: statusIcon parent: JobIcon id: JobIconSeniorEngineer icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorEngineer - type: statusIcon parent: JobIcon id: JobIconSeniorResearcher icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: SeniorResearcher - type: statusIcon parent: JobIcon id: JobIconVisitor icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Visitor diff --git a/Resources/Prototypes/StatusIcon/antag.yml b/Resources/Prototypes/StatusIcon/antag.yml index 0a052ce16da..3b64517530b 100644 --- a/Resources/Prototypes/StatusIcon/antag.yml +++ b/Resources/Prototypes/StatusIcon/antag.yml @@ -2,21 +2,21 @@ id: ZombieFaction priority: 11 icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Zombie - type: statusIcon id: RevolutionaryFaction priority: 11 icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Revolutionary - type: statusIcon id: HeadRevolutionaryFaction priority: 11 icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: HeadRevolutionary - type: statusIcon @@ -25,7 +25,7 @@ locationPreference: Right layer: Mod icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: MindShield - type: statusIcon @@ -33,5 +33,5 @@ priority: 0 locationPreference: Left icon: - sprite: Interface/Misc/job_icons.rsi + sprite: /Textures/Interface/Misc/job_icons.rsi state: Syndicate diff --git a/Resources/Prototypes/StatusIcon/debug.yml b/Resources/Prototypes/StatusIcon/debug.yml index d230fa852c7..2011c6ceeae 100644 --- a/Resources/Prototypes/StatusIcon/debug.yml +++ b/Resources/Prototypes/StatusIcon/debug.yml @@ -1,19 +1,19 @@ - type: statusIcon id: DebugStatus icon: - sprite: Interface/Misc/research_disciplines.rsi + sprite: /Textures/Interface/Misc/research_disciplines.rsi state: civilianservices - type: statusIcon id: DebugStatus2 priority: 1 icon: - sprite: Interface/Misc/research_disciplines.rsi + sprite: /Textures/Interface/Misc/research_disciplines.rsi state: arsenal - type: statusIcon id: DebugStatus3 priority: 5 icon: - sprite: Interface/Misc/research_disciplines.rsi + sprite: /Textures/Interface/Misc/research_disciplines.rsi state: experimental diff --git a/Resources/Prototypes/Tiles/floors.yml b/Resources/Prototypes/Tiles/floors.yml index 33469c404ae..425af690e76 100644 --- a/Resources/Prototypes/Tiles/floors.yml +++ b/Resources/Prototypes/Tiles/floors.yml @@ -1813,6 +1813,7 @@ collection: FootstepHull itemDrop: FloorTileItemSteel heatCapacity: 100000 #/tg/ has this set as "INFINITY." I don't know if that exists here so I've just added an extra 0 + indestructible: true - type: tile id: FloorReinforcedHardened diff --git a/Resources/Prototypes/Tiles/plating.yml b/Resources/Prototypes/Tiles/plating.yml index 3164e77fd22..5e888fbf6ee 100644 --- a/Resources/Prototypes/Tiles/plating.yml +++ b/Resources/Prototypes/Tiles/plating.yml @@ -73,3 +73,18 @@ itemDrop: PartRodMetal1 heatCapacity: 10000 +- type: tile + id: TrainLattice + name: tiles-lattice-train + sprite: /Textures/Tiles/latticeTrain.png + baseTurf: Space + isSubfloor: true + deconstructTools: [ Cutting ] + weather: true + footstepSounds: + collection: FootstepPlating + friction: 0.3 + isSpace: true + itemDrop: PartRodMetal1 + heatCapacity: 10000 + diff --git a/Resources/Prototypes/Voice/speech_emote_sounds.yml b/Resources/Prototypes/Voice/speech_emote_sounds.yml index 8df129f7869..3740b995212 100644 --- a/Resources/Prototypes/Voice/speech_emote_sounds.yml +++ b/Resources/Prototypes/Voice/speech_emote_sounds.yml @@ -355,3 +355,9 @@ path: /Audio/Animals/kangaroo_grunt.ogg params: variation: 0.125 + +- type: emoteSounds + id: Nymph + sounds: + Chirp: + path: /Audio/Animals/nymph_chirp.ogg diff --git a/Resources/Prototypes/Voice/speech_emotes.yml b/Resources/Prototypes/Voice/speech_emotes.yml index 86d273dd1c4..0ee67d17c78 100644 --- a/Resources/Prototypes/Voice/speech_emotes.yml +++ b/Resources/Prototypes/Voice/speech_emotes.yml @@ -241,3 +241,21 @@ - buzzes - buzzes. - buzzes! + +- type: emote + id: Chirp + category: Vocal + chatMessages: [chirps!] + chatTriggers: + - chirp + - chirp! + - chirp. + - chirps + - churps. + - chirps! + - chirped + - chirped. + - chirped! + - chirping + - chirping. + - chirping! \ No newline at end of file diff --git a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml index 9bcbfe44773..0fdaa5b9565 100644 --- a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml @@ -47,6 +47,8 @@ components: - Item # it doesnt necessarily have to be restricted from structures, but i think it'll be better that way permanentComponents: + - type: Item + size: Huge - type: ContainerContainer containers: storagebase: !type:Container @@ -147,6 +149,12 @@ permanentComponents: - type: PowerSupplier supplyRate: 20000 + - type: NodeContainer + examinable: true + nodes: + output_hv: + !type:CableDeviceNode + nodeGroupID: HVPower - type: artifactEffect id: EffectBigIron diff --git a/Resources/Prototypes/audio.yml b/Resources/Prototypes/audio.yml index b5ab41091b6..666bac4a2f2 100644 --- a/Resources/Prototypes/audio.yml +++ b/Resources/Prototypes/audio.yml @@ -25,6 +25,15 @@ rules: NearPrayable priority: 4 +- type: ambientMusic + id: Train + sound: + params: + volume: -8 + collection: AmbienceTrain + rules: NearTrain + priority: 4 + # Departments - type: ambientMusic id: Medical @@ -205,6 +214,16 @@ - /Audio/Ambience/ambiruin6.ogg - /Audio/Ambience/ambiruin7.ogg +- type: soundCollection + id: AmbienceTrain + files: + - /Audio/Ambience/ambitrain1.ogg + - /Audio/Ambience/ambitrain2.ogg + - /Audio/Ambience/ambitrain3.ogg + - /Audio/Ambience/ambiruin3.ogg + - /Audio/Ambience/ambiruin5.ogg + - /Audio/Ambience/ambiruin6.ogg + - type: soundCollection id: AmbienceSpookyFog files: @@ -262,6 +281,16 @@ - Plating range: 2 +- type: rules + id: NearTrain + rules: + - !type:NearbyTilesPercentRule + ignoreAnchored: true + percent: 0.05 + tiles: + - TrainLattice + range: 4 + - type: rules id: NearMedical rules: diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/MindShield.png b/Resources/Textures/Interface/Misc/job_icons.rsi/MindShield.png index d3df5fe6277..e311b9f6163 100644 Binary files a/Resources/Textures/Interface/Misc/job_icons.rsi/MindShield.png and b/Resources/Textures/Interface/Misc/job_icons.rsi/MindShield.png differ diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json index df37d244cfe..dd9e51c3d18 100644 --- a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json +++ b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/master/icons/mob/huds/hud.dmi and edited by Golinth (Github)", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/master/icons/mob/huds/hud.dmi", "size": { "x": 8, @@ -171,7 +171,11 @@ "name": "HeadRevolutionary" }, { - "name": "MindShield" + "name": "MindShield", + "delays": + [ + [0.5,0.5] + ] }, { "name": "Syndicate" diff --git a/Resources/Textures/Interface/NavMap/beveled_circle.png.yml b/Resources/Textures/Interface/NavMap/beveled_circle.png.yml new file mode 100644 index 00000000000..dabd6601f78 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_circle.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_hexagon.png.yml b/Resources/Textures/Interface/NavMap/beveled_hexagon.png.yml new file mode 100644 index 00000000000..dabd6601f78 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_hexagon.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_square.png.yml b/Resources/Textures/Interface/NavMap/beveled_square.png.yml new file mode 100644 index 00000000000..dabd6601f78 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_square.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/NavMap/beveled_triangle.png.yml b/Resources/Textures/Interface/NavMap/beveled_triangle.png.yml new file mode 100644 index 00000000000..dabd6601f78 --- /dev/null +++ b/Resources/Textures/Interface/NavMap/beveled_triangle.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/icon-vend.png b/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/icon-vend.png new file mode 100644 index 00000000000..765112be764 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/icon-vend.png differ diff --git a/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/meta.json b/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/meta.json index 6dfbe5d8c0c..b22da85aa90 100644 --- a/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Drinks/hot_coco.rsi/meta.json @@ -20,6 +20,9 @@ }, { "name": "icon-4" + }, + { + "name": "icon-vend" } ] } diff --git a/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/icon-vend.png b/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/icon-vend.png new file mode 100644 index 00000000000..765112be764 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/icon-vend.png differ diff --git a/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/meta.json b/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/meta.json index 6dfbe5d8c0c..b22da85aa90 100644 --- a/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Drinks/hot_coffee.rsi/meta.json @@ -20,6 +20,9 @@ }, { "name": "icon-4" + }, + { + "name": "icon-vend" } ] } diff --git a/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-green-tea.png b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-green-tea.png new file mode 100644 index 00000000000..5c83dd91ad7 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-green-tea.png differ diff --git a/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-tea.png b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-tea.png new file mode 100644 index 00000000000..771497c2293 Binary files /dev/null and b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/icon-vend-tea.png differ diff --git a/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/meta.json b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/meta.json index 6e813512e7c..2666be145a8 100644 --- a/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Drinks/teacup.rsi/meta.json @@ -21,6 +21,12 @@ }, { "name": "icon-4" + }, + { + "name": "icon-vend-tea" + }, + { + "name": "icon-vend-green-tea" } ] } \ No newline at end of file diff --git a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-left.png b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-left.png index 1e45f5ee18b..50e46e896c6 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-left.png and b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-right.png b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-right.png index df8b5fbebe0..27afb87dd59 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-right.png and b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce-inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce.png index 88ae7431771..6a33930e2f6 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/death_nettle.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-left.png b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-left.png index d6a98c8cf25..3a1463ee177 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-left.png and b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-right.png b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-right.png index bd346bdfed8..fc9a24a94ef 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-right.png and b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce-inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce.png index 3b50100cd0f..035b6ff8ba6 100644 Binary files a/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce.png and b/Resources/Textures/Objects/Specific/Hydroponics/nettle.rsi/produce.png differ diff --git a/Resources/Textures/Parallaxes/Asteroids.png b/Resources/Textures/Parallaxes/Asteroids.png new file mode 100644 index 00000000000..fd5f4b6e150 Binary files /dev/null and b/Resources/Textures/Parallaxes/Asteroids.png differ diff --git a/Resources/Textures/Parallaxes/attributions.yml b/Resources/Textures/Parallaxes/attributions.yml index 71f0c919d29..fac31a8c0b5 100644 --- a/Resources/Textures/Parallaxes/attributions.yml +++ b/Resources/Textures/Parallaxes/attributions.yml @@ -6,4 +6,19 @@ - files: ["KettleParallaxNeb.png", "KettleParallaxBG.png", "AspidParallaxBG.png", "AspidParallaxBG.png"] license: "CC0-1.0" copyright: "adapted from Screaming Brain Studios" - source: "https://opengameart.org/content/seamless-space-backgrounds" \ No newline at end of file + source: "https://opengameart.org/content/seamless-space-backgrounds" + +- files: ["gas_giant.png"] + license: "CC-BY-NC-SA-3.0" + copyright: "made by brainfood1183 (github) for ss14" + source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Textures/Parallaxes/gas_giant.png" + +- files: ["Asteroids.png"] + license: "CC-BY-NC-SA-3.0" + copyright: "taken from tgstation on https://github.com/tgstation/tgstation/commit/3df5d3b42bfb6b3b5adba1067ab41f83816255bb from parallax.dmi" + source: "https://github.com/tgstation/tgstation" + +- files: ["core_planet.png"] + license: "CC-BY-NC-SA-3.0" + copyright: "Drawn by Ubaser" + source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Textures/Parallaxes/core_planet.png" diff --git a/Resources/Textures/Tiles/attributions.yml b/Resources/Textures/Tiles/attributions.yml index e9795a358bc..622565f2511 100644 --- a/Resources/Textures/Tiles/attributions.yml +++ b/Resources/Textures/Tiles/attributions.yml @@ -115,3 +115,8 @@ license: "CC-BY-SA-3.0" copyright: "taken at https://github.com/ParadiseSS13/Paradise/commit/43889a89d5a9378fd120d627f74613edb1841a66" source: "https://github.com/ParadiseSS13/Paradise" + +- files: ["latticeTrain.png"] + license: "CC0-1.0" + copyright: "Created by TheShuEd (github) for space-station-14." + source: "https://github.com/space-wizards/space-station-14/pull/ED_INSERT_PR_CODE_HERE!" diff --git a/Resources/Textures/Tiles/latticeTrain.png b/Resources/Textures/Tiles/latticeTrain.png new file mode 100644 index 00000000000..72905e4a41b Binary files /dev/null and b/Resources/Textures/Tiles/latticeTrain.png differ diff --git a/flake.lock b/flake.lock index 6cfa2ba415a..6ab38fa41bd 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1706213598, - "narHash": "sha256-Toz3HEHeq6Esr5uDOMel8BiGSa94gj+og3Yz4YEgjYI=", + "lastModified": 1708210246, + "narHash": "sha256-Q8L9XwrBK53fbuuIFMbjKvoV7ixfLFKLw4yV+SD28Y8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cda0e75a0bd7cf05bd3e40658c163e4f8f376b7b", + "rev": "69405156cffbdf2be50153f13cbdf9a0bea38e49", "type": "github" }, "original": { diff --git a/shell.nix b/shell.nix index d3ece6cdb14..da363feda9c 100644 --- a/shell.nix +++ b/shell.nix @@ -1,8 +1,9 @@ -{ pkgs ? import (builtins.fetchTarball { +{ pkgs ? (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); +in import (builtins.fetchTarball { url = - "https://github.com/NixOS/nixpkgs/archive/cda0e75a0bd7cf05bd3e40658c163e4f8f376b7b.tar.gz"; - sha256 = "sha256-Toz3HEHeq6Esr5uDOMel8BiGSa94gj+og3Yz4YEgjYI="; -}) { } }: + "https://github.com/NixOS/nixpkgs/archive/${lock.nodes.nixpkgs.locked.rev}.tar.gz"; + sha256 = lock.nodes.nixpkgs.locked.narHash; +}) { }) }: let dependencies = with pkgs; [ @@ -41,6 +42,7 @@ let dbus at-spi2-core cups + python3 ]; in pkgs.mkShell { name = "space-station-14-devshell";